File Coverage

blib/lib/Data/Sah/Resolve.pm
Criterion Covered Total %
statement 38 107 35.5
branch 6 34 17.6
condition 0 6 0.0
subroutine 7 8 87.5
pod 1 1 100.0
total 52 156 33.3


line stmt bran cond sub pod time code
1             package Data::Sah::Resolve;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2021-07-29'; # DATE
5             our $DIST = 'Data-Sah-Resolve'; # DIST
6             our $VERSION = '0.011'; # VERSION
7              
8 1     1   97880 use 5.010001;
  1         11  
9 1     1   4 use strict;
  1         2  
  1         19  
10 1     1   5 use warnings;
  1         2  
  1         44  
11              
12 1     1   5 use Exporter qw(import);
  1         2  
  1         319  
13             our @EXPORT_OK = qw(resolve_schema);
14              
15             sub _clset_has_merge {
16 0     0   0 my $clset = shift;
17 0         0 for (keys %$clset) {
18 0 0       0 return 1 if /\Amerge\./;
19             }
20 0         0 0;
21             }
22              
23             sub _resolve {
24 1     1   4 my ($opts, $res) = @_;
25              
26 1         3 my $type = $res->{type};
27             die "Cannot resolve Sah schema: circular schema definition: ".
28 0         0 join(" -> ", @{$res->{resolve_path}}, $type)
29 1 50       2 if grep { $type eq $_ } @{$res->{resolve_path}};
  0         0  
  1         5  
30              
31 1         3 unshift @{$res->{resolve_path}}, $type;
  1         3  
32              
33             # check whether $type is a built-in Sah type
34 1         5 (my $typemod_pm = "Data/Sah/Type/$type.pm") =~ s!::!/!g;
35 1         2 eval { require $typemod_pm; 1 };
  1         172  
  0         0  
36 1         24 my $err = $@;
37 1 50       4 unless ($err) {
38             # already a builtin-type, so we stop here
39 0         0 return;
40             }
41 1 50       7 die "Cannot resolve Sah schema: can't check whether $type is a builtin Sah type: $err"
42             unless $err =~ /\ACan't locate/;
43              
44             # not a type, try a schema under Sah::Schema
45 1         3 my $schmod = "Sah::Schema::$type";
46 1         6 (my $schmod_pm = "$schmod.pm") =~ s!::!/!g;
47 1         2 eval { require $schmod_pm; 1 };
  1         209  
  0         0  
48 1 50       22 die "Cannot resolve Sah schema: not a known built-in Sah type '$type' (can't locate ".
49             "Data::Sah::Type::$type) and not a known schema name '$type' ($@)"
50             if $@;
51 1     1   7 no strict 'refs';
  1         2  
  1         716  
52 0         0 my $sch2 = ${"$schmod\::schema"};
  0         0  
53 0 0       0 die "Cannot resolve Sah schema: BUG: Schema module $schmod doesn't contain \$schema"
54             unless $sch2;
55 0         0 $res->{type} = $sch2->[0];
56 0         0 unshift @{ $res->{clsets_after_type} }, $sch2->[1];
  0         0  
57 0         0 _resolve($opts, $res);
58             }
59              
60             sub resolve_schema {
61 1 50   1 1 1885 my $opts = ref($_[0]) eq 'HASH' ? shift : {};
62 1         3 my $sch = shift;
63              
64             # normalize
65 1 50       4 unless ($opts->{schema_is_normalized}) {
66 1         573 require Data::Sah::Normalize;
67 1         1351 $sch = Data::Sah::Normalize::normalize_schema($sch);
68             }
69              
70 1         50 my $res = {
71             v => 2,
72             type => $sch->[0],
73             clsets_after_type => [$sch->[1]],
74             resolve_path => [],
75             };
76              
77             # resolve
78 1         3 _resolve($opts, $res);
79              
80             # determine the "base restrictions" base
81 0           my @clsets_have_merge;
82             my $has_merge_prefixes; # whether any of the clsets have merge prefixes
83 0           for (@{ $res->{clsets_after_type} }) {
  0            
84 0           push @clsets_have_merge, _clset_has_merge($_);
85 0 0         $has_merge_prefixes++ if $clsets_have_merge[-1];
86             }
87             # TODO: sanity check: the innermost base schema should not have merge prefixes
88 0           my $idx = $#clsets_have_merge;
89 0           while ($idx >= 0) {
90 0 0         if ($opts->{allow_base_with_no_additional_clauses}) {
91 0 0         last if !$clsets_have_merge[$idx];
92             } else {
93 0 0 0       last if keys(%{$res->{clsets_after_type}[$idx]}) > 0 && !$clsets_have_merge[$idx];
  0            
94             }
95 0           $idx--;
96             }
97             #use DD; dd $res->{clsets_after_type}; dd \@clsets_have_merge;
98 0           $res->{base} = $res->{resolve_path}[$idx];
99 0           $res->{clsets_after_base} = [grep {keys(%$_) > 0} @{ $res->{clsets_after_type} }[$idx .. $#clsets_have_merge]];
  0            
  0            
100              
101             # merge
102 0           my @merged_clsets;
103             MERGE: {
104 0 0         unless ($has_merge_prefixes) {
  0            
105 0           @merged_clsets = grep { keys(%$_)>0 } @{ $res->{clsets_after_type} };
  0            
  0            
106 0           last;
107             }
108 0           @merged_clsets = ($res->{clsets_after_type}[0]);
109 0           for my $i (1 .. $#clsets_have_merge) {
110 0           my $clset = $res->{clsets_after_type}[$i];
111 0 0         next unless keys(%$clset) > 0;
112 0 0         if ($clsets_have_merge[$i]) {
113 0           state $merger = do {
114 0           require Data::ModeMerge;
115 0           my $mm = Data::ModeMerge->new(config => {
116             recurse_array => 1,
117             });
118 0           $mm->modes->{NORMAL} ->prefix ('merge.normal.');
119 0           $mm->modes->{NORMAL} ->prefix_re(qr/\Amerge\.normal\./);
120 0           $mm->modes->{ADD} ->prefix ('merge.add.');
121 0           $mm->modes->{ADD} ->prefix_re(qr/\Amerge\.add\./);
122 0           $mm->modes->{CONCAT} ->prefix ('merge.concat.');
123 0           $mm->modes->{CONCAT} ->prefix_re(qr/\Amerge\.concat\./);
124 0           $mm->modes->{SUBTRACT}->prefix ('merge.subtract.');
125 0           $mm->modes->{SUBTRACT}->prefix_re(qr/\Amerge\.subtract\./);
126 0           $mm->modes->{DELETE} ->prefix ('merge.delete.');
127 0           $mm->modes->{DELETE} ->prefix_re(qr/\Amerge\.delete\./);
128 0           $mm->modes->{KEEP} ->prefix ('merge.keep.');
129 0           $mm->modes->{KEEP} ->prefix_re(qr/\Amerge\.keep\./);
130 0           $mm;
131             };
132 0           my $merge_res = $merger->merge($merged_clsets[-1], $clset);
133 0 0         unless ($merge_res->{success}) {
134 0           die "Can't resolve schema: Can't merge clause set: $merge_res->{error}";
135             }
136 0           $merged_clsets[-1] = $merge_res->{result};
137             } else {
138 0           push @merged_clsets, $clset;
139             }
140             } # for clause set
141             } # MERGE
142 0 0 0       pop @merged_clsets if @merged_clsets && keys(%{$merged_clsets[-1]}) == 0;
  0            
143 0           $res->{'clsets_after_type.alt.merge.merged'} = \@merged_clsets;
144              
145 0           $res;
146             }
147              
148             1;
149             # ABSTRACT: Resolve Sah schema
150              
151             __END__
152              
153             =pod
154              
155             =encoding UTF-8
156              
157             =head1 NAME
158              
159             Data::Sah::Resolve - Resolve Sah schema
160              
161             =head1 VERSION
162              
163             This document describes version 0.011 of Data::Sah::Resolve (from Perl distribution Data-Sah-Resolve), released on 2021-07-29.
164              
165             =head1 SYNOPSIS
166              
167             use Data::Sah::Resolve qw(resolve_schema);
168              
169             my $sch = resolve_schema("int");
170             # => {
171             # v => 2,
172             # type=>"int",
173             # clsets_after_type => [],
174             # "clsets_after_type.alt.merge.merged" => [],
175             # base=>"int",
176             # clsets_after_base => [],
177             # resolve_path => ["int"],
178             # }
179              
180             my $sch = resolve_schema("posint*");
181             # => {
182             # v => 2,
183             # type=>"int",
184             # clsets_after_type => [{min=>1}, {req=>1}],
185             # "clsets_after_type.alt.merge.merged" => [{min=>1}, {req=>1}],
186             # base => "posint",
187             # clsets_after_base => [{req=>1}],
188             # resolve_path => ["int","posint"],
189             # }
190              
191             my $sch = resolve_schema([posint => div_by => 3]);
192             # => {
193             # v => 2,
194             # type=>"int",
195             # clsets_after_type => [{min=>1}, {div_by=>3}],
196             # "clsets_after_type.alt.merge.merged" => [{min=>1}, {div_by=>3}],
197             # base => "posint",
198             # clsets_after_base => [{div_by=>3}],
199             # resolve_path => ["int","posint"],
200             # }
201             # => ["int", {min=>1}, {div_by=>3}]
202              
203             my $sch = resolve_schema(["posint", "merge.delete.min"=>undef, div_by => 3]);
204             # basically becomes: ["int", div_by=>3]
205             # => {
206             # v => 2,
207             # type=>"int",
208             # clsets_after_type => [{min=>1}, {"merge.delete.min"=>undef, div_by=>3}],
209             # "clsets_after_type.alt.merge.merged" => [{div_by=>3}],
210             # base => undef,
211             # clsets_after_base => [{div_by=>3}],
212             # resolve_path => ["int","posint"],
213             # }
214             # => ["int", {min=>1}, {div_by=>3}]
215              
216             =head1 DESCRIPTION
217              
218             This module provides L</resolve_schema>.
219              
220             =head1 FUNCTIONS
221              
222             =head2 resolve_schema
223              
224             Usage:
225              
226             my $res = resolve_schema([ \%opts, ] $sch); # => hash
227              
228             Sah schemas can be defined in terms of other schemas as base. The resolving
229             process follows the (outermost) base schema until it finds a builtin type as the
230             (innermost) base. It then returns a hash result (a L<DefHash> with C<v>=2)
231             containing the type as well other information like the collected clause sets and
232             others.
233              
234             This routine performs the following steps:
235              
236             =over
237              
238             =item 1. Normalize the schema
239              
240             Unless C<schema_is_normalized> option is true, in which case schema is assumed
241             to be normalized already.
242              
243             =item 2. Check if the schema's type is a builtin type
244              
245             Currently this is done by checking if the module of the name C<<
246             Data::Sah::Type::<type> >> is loadable. If it is a builtin type then we are
247             done.
248              
249             =item 3. Check if the schema's type is the name of another schema
250              
251             This is done by checking if C<< Sah::Schema::<name> >> module exists and is
252             loadable. If this is the case then we retrieve the base schema from the
253             C<$schema> variable in the C<< Sah::Schema::<name> >> package and repeat the
254             process while accumulating and/or merging the clause sets.
255              
256             =item 4. If schema's type is neither, we die.
257              
258             =back
259              
260             Will also die on circularity or when there is other failures like failing to get
261             schema from the schema module.
262              
263             Example 1: C<int>.
264              
265             First we normalize to C<< ["int",{}] >>. The type is C<int> and it is a builtin
266             type (L<Data::Sah::Type::int> exists). The final result is:
267              
268             {
269             v => 2,
270             type=>"int",
271             clsets_after_type => [],
272             "clsets_after_type.alt.merge.unmerged" => [],
273             base=>undef,
274             clsets_after_base => [],
275             resolve_path => ["int"],
276             }
277              
278             Example 2: C<posint*>.
279              
280             First we normalize to C<< ["posint",{req=>1}] >>. The type part of this schema
281             is C<posint> and it is actually the name of another schema because
282             C<Data::Sah::Type::posint> is not found and we find schema module
283             L<Sah::Schema::posint>) instead. We then retrieve the C<posint> schema from the
284             schema module's C<$schema> and we get C<< ["int", {min=>1}] >> (additional
285             informative clauses omitted for brevity). We now try to resolve C<int> and find
286             that it's a builtin type. So the final result is:
287              
288             {
289             v => 2,
290             type=>"int",
291             clsets_after_type => [{min=>1}, {req=>1}],
292             "clsets_after_type.alt.merge.unmerged" => [{min=>1}, {req=>1}],
293             base => "posint",
294             clsets_after_base => [{req=>1}],
295             resolve_path => ["int","posint"],
296             }
297              
298             Known options:
299              
300             =over
301              
302             =item * schema_is_normalized
303              
304             Bool, default false. When set to true, function will skip normalizing schema and
305             assume input schema is normalized.
306              
307             =item * allow_base_with_no_additional_clauses
308              
309             Bool, default false. Normally, a schema like C<< "posint" >> or C<<
310             ["posint",{}] >> will result in C<"int"> as the base (because the schema does
311             not add any additional clauses to the "posint" schema) while C<<
312             ["posint",{div_by=>2}] >> will result in C<"posint"> as the base. But if this
313             setting is set to true, then all the previous examples will result in
314             C<"posint"> as the base.
315              
316             =back
317              
318             As mentioned, result is a hash conforming to the L<DefHash> restriction. The
319             following keys will be returned:
320              
321             =over
322              
323             =item * v
324              
325             Integer, has the value of 2. A non-compatible change of result will bump this
326             version number.
327              
328             =item * type
329              
330             Str, the Sah builtin type name.
331              
332             =item * clsets_after_type
333              
334             All the collected clause sets, from the deepest base schema to the outermost,
335             and to the clause set of the original unresolved schema.
336              
337             =item * clsets_after_type.alt.merge.merged
338              
339             Like L</clsets_after_type>, but the clause sets are merged according to the
340             L<Sah> merging specification.
341              
342             =item * base
343              
344             Str. Might be undef. The outermost base schema (or type) that can be used as
345             "base restriction", meaning its restrictions (clause sets) must all be
346             fulfilled. After this base's clause sets, the next additional clause sets will
347             not contain any merge prefixes. Because if additional clause sets contained
348             merge prefixes, they could modify or remove restrictions set by the base instead
349             of just adding more restrictions (which is the whole point of merging).
350              
351             =item * clsets_after_base
352              
353             Clause sets after the "base restriction" base. This is additional restrictions
354             that are imposed to the restrictions of the base schema. They do not contain
355             merge prefixes.
356              
357             =item * resolve_path
358              
359             Array. This is a list of schema type names or builtin type names, from the
360             deepest to the shallowest. The first element of this arrayref is the builtin Sah
361             type and the last element is the original unresolved schema's type.
362              
363             =back
364              
365             =head1 HOMEPAGE
366              
367             Please visit the project's homepage at L<https://metacpan.org/release/Data-Sah-Resolve>.
368              
369             =head1 SOURCE
370              
371             Source repository is at L<https://github.com/perlancar/perl-Data-Sah-Resolve>.
372              
373             =head1 BUGS
374              
375             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Data-Sah-Resolve>
376              
377             When submitting a bug or request, please include a test-file or a
378             patch to an existing test-file that illustrates the bug or desired
379             feature.
380              
381             =head1 SEE ALSO
382              
383             L<Sah>, L<Data::Sah>
384              
385             =head1 AUTHOR
386              
387             perlancar <perlancar@cpan.org>
388              
389             =head1 COPYRIGHT AND LICENSE
390              
391             This software is copyright (c) 2021, 2017, 2016 by perlancar@cpan.org.
392              
393             This is free software; you can redistribute it and/or modify it under
394             the same terms as the Perl 5 programming language system itself.
395              
396             =cut