File Coverage

blib/lib/Data/Annotation/Expression.pm
Criterion Covered Total %
statement 117 141 82.9
branch 27 64 42.1
condition 5 19 26.3
subroutine 17 18 94.4
pod 8 8 100.0
total 174 250 69.6


line stmt bran cond sub pod time code
1             package Data::Annotation::Expression;
2 3     3   138442 use v5.24;
  3         16  
3 3     3   636 use utf8;
  3         352  
  3         18  
4 3     3   97 use warnings;
  3         6  
  3         174  
5 3     3   625 use experimental qw< signatures >;
  3         5064  
  3         1999  
6             { our $VERSION = '0.006' }
7              
8 3     3   2344 use Data::Annotation::Traverse qw< crumble traverse_plain >;
  3         10  
  3         293  
9 3     3   19 use Exporter qw< import >;
  3         6  
  3         6368  
10             our @EXPORT_OK = qw< evaluator_factory >;
11              
12 5     5 1 816 sub evaluator_factory ($definition, $parse_ctx = {}) {
  5         10  
  5         12  
  5         9  
13 5         20 my %parse_ctx = $parse_ctx->%*;
14              
15             # the ::Builtin is injected by default to support
16             # built-ins. It's possible to disable it but only explicitly, providing
17             # a true value for key 'no-builtin'.
18 5 50       21 if (! $parse_ctx{'no-builtin'}) {
19 5   50     29 my @prefixes = ($parse_ctx{'locator-relative-prefixes'} // [])->@*;
20 5         18 push @prefixes, qw< Data::Annotation::Expression::Builtin >;
21 5         16 $parse_ctx{'locator-relative-prefixes'} = \@prefixes;
22             }
23              
24 5         17 return generate_function(\%parse_ctx, $definition);
25             }
26              
27             ########################################################################
28             #
29             # Private part follows
30              
31 19     19 1 33 sub default_definition_normalizer ($parse_ctx, $definition) {
  19         31  
  19         37  
  19         26  
32 19 50       60 die "undefined definition\n" unless defined($definition);
33              
34             # if it's a string definition... best wishes!
35 19 100       58 if (ref($definition) eq '') {
36 12 50       30 return { type => 'data', value => '' } unless length($definition);
37              
38 12 50 0     33 return { type => 'context', path => ($1 // '') }
39             if $definition =~ m{\A context (?: \z | \. (.*)) }mxs;
40              
41 12         31 my $first = substr($definition, 0, 1);
42 12         34 my $rest = substr($definition, 1);
43 12 100       56 return { type => 'context', path => "run.$rest" } if $first eq '.';
44 5 50       33 return { type => 'data', value => $rest } if $first eq '=';
45 0 0       0 return { type => 'sub', name => $rest } if $first eq '&';
46              
47             # complain loudly here, i.e. unhandled first characters...
48 0 0       0 die "cannot parse definition '$definition'\n"
49             unless ref($definition);
50             }
51              
52 7         56 my %copy = $definition->%*;
53              
54             # if the definition contains "type" then it's expected to *almost* in
55             # normalized form. We might admit that args were not provided in
56             # subs and set them to an empty array reference in this case.
57 7 50       26 if (exists($copy{type})) { # should be mostly fine
58 0         0 my $type = $copy{type};
59 0 0       0 if (! defined($type)) { die "undefined type in definition\n" }
  0 0       0  
    0          
60             elsif ($type eq 'data') {
61             die "missing value for data request in definition\n"
62 0 0       0 unless exists($copy{value});
63 0         0 return \%copy;
64             }
65             elsif ($type eq 'sub') {
66             die "missing locator of required sub in definition\n"
67 0 0       0 unless exists($copy{name});
68 0   0     0 $copy{args} //= [];
69             }
70             else { } # nothing to check, normalization complete
71 0         0 return \%copy;
72             }
73              
74 7 50       21 if (scalar(keys(%copy)) == 1) { # we're in DWIM land here
75 7         22 my ($key, $value) = %copy;
76 7 50       20 return { type => 'data', value => $value }
77             if $key eq 'data';
78 7 50       21 return { type => 'sub', name => $value, args => [] }
79             if $key eq 'sub';
80 7 50       21 if ($key eq 'context') {
81 0 0 0     0 $value = 'run' . $value
      0        
82             if length($value // '') && substr($value, 0, 1) eq '.';
83 0         0 return { type => 'context', path => $value };
84             }
85 7 50       25 return { type => 'sub', package => $1, name => $2, args => $value }
86             if $key =~ m{\A (.+) (?: :: | /) (.+)}mxs;
87 7         47 return { type => 'sub', name => $key, args => $value };
88             }
89             else {
90 0         0 die "cannot normalize definition\n";
91             }
92              
93             # should never be reached
94             ...
95 0         0 }
96              
97 19     19 1 33 sub generate_function ($parse_ctx, $definition) {
  19         31  
  19         34  
  19         26  
98             my $normalizer = exists($parse_ctx->{'definition-normalizer'})
99 19 50       153 ? $parse_ctx->{'definition-normalizer'}
100             : __PACKAGE__->can('default_definition_normalizer');
101 19 50       73 $definition = $normalizer->($parse_ctx, $definition)
102             if defined($normalizer);
103              
104 19         49 my $type = $definition->{type};
105 19 50       128 my $parser = __PACKAGE__->can("generate_function_$type")
106             or die "no parser for function type '$type'\n";
107 19         61 return $parser->($parse_ctx, $definition);
108             }
109              
110 5     5 1 28 sub generate_function_data ($parse_ctx, $definition) {
  5         8  
  5         10  
  5         8  
111 5     7   37 return sub ($overlay) { return $definition->{value} };
  7         33  
  7         12  
  7         13  
  7         10  
112             }
113              
114 7     7 1 12 sub generate_function_context ($parse_ctx, $definition) {
  7         11  
  7         16  
  7         9  
115 7   50     24 my $path = $definition->{path} // '';
116 7         34 my ($entry, @crumbs) = crumble($path)->@*;
117              
118             # the runtime context is a Data::Annotation::Overlay instance. For
119             # 'run' we plug directly into it, otherwise we use its access options
120             # for other data, but without the overlay/caching
121 7 50   11   79 return sub ($overlay) { $overlay->get(\@crumbs) } if $entry eq 'run';
  11         49  
  11         19  
  11         20  
  11         17  
122 0         0 my $other = { definition => $definition, parse => $parse_ctx };
123 0     0   0 return sub ($overlay) {
  0         0  
  0         0  
124 0         0 return $overlay->get_external([$entry, @crumbs], $other);
125 0         0 };
126             }
127              
128 7     7 1 10 sub generate_function_sub ($parse_ctx, $definition) {
  7         11  
  7         22  
  7         11  
129 7         23 my ($name, $package) = $definition->@{qw< name package >};
130 7         21 my $function = resolve_function($parse_ctx, $name, $package);
131 14         44 my @args = map { generate_function($parse_ctx, $_) }
132 7   50     46 ($definition->{args} // [])->@*;
133 7     11   73 return sub ($overlay) { $function->(map { $_->($overlay) } @args) };
  11         25  
  22         52  
  11         671  
  11         18  
  11         18  
134             }
135              
136 7     7 1 11 sub resolve_function ($parse_ctx, $name, $package) {
  7         17  
  7         44  
  7         11  
  7         17  
137 7 50       18 die "undefined sub name\n" unless defined($name);
138 7 50       19 die "empty sub name\n" unless length($name);
139              
140 7   50     33 my $suffix = $package //= '';
141 7         20 my $is_absolute = $suffix =~ s{\A /}{}mxs;
142 7         25 my $relative_prefixes = $parse_ctx->{'locator-relative-prefixes'};
143 7 50 50     33 my @prefixes = $is_absolute ? ('') : (($relative_prefixes // [])->@*);
144              
145 7         13 my $function;
146             PREFIX:
147 7         18 for my $prefix (@prefixes) {
148 7         18 my $module = join('::', grep { length } ($prefix, $suffix));
  14         42  
149             #warn "module<$module> name<$name>";
150              
151 7         23 for (1 .. 2) { # first try directly, then require $module
152 9 100       136 if (my $factory = $module->can('factory')) {
    50          
153 7         30 $function = $factory->($parse_ctx, $name);
154 7 50       39 return $function if defined($function);
155 0         0 next PREFIX; # if a factory exists, no more attempts anyway
156             }
157             elsif ($function = $module->can($name)) {
158 0         0 return $function;
159             }
160             else { # prepare for next attempt, if we still have one
161             #warn "Loading module <$module>";
162 2 50       5 eval { require_module($module) } or do {
  2         8  
163             #warn "error: $@";
164 0         0 next PREFIX;
165             };
166             #warn "Loaded module <$module>";
167             }
168             }
169             }
170              
171 0         0 die "cannot find sub for '$name'\n";
172             }
173              
174 2     2 1 4 sub require_module ($module) {
  2         5  
  2         5  
175 2         16 my $path = "$module.pm" =~ s{::}{/}rgmxs;
176 2         1518 require $path;
177             }
178              
179             1;