File Coverage

lib/Context/Singleton/Frame.pm
Criterion Covered Total %
statement 141 148 95.2
branch 29 32 90.6
condition 5 9 55.5
subroutine 41 43 95.3
pod 0 15 0.0
total 216 247 87.4


line stmt bran cond sub pod time code
1              
2 3     3   84272 use v5.10;
  3         11  
3 3     3   13 use strict;
  3         4  
  3         49  
4 3     3   11 use warnings;
  3         4  
  3         108  
5              
6             package Context::Singleton::Frame;
7              
8             our $VERSION = v1.0.5;
9              
10 3     3   13 use List::Util;
  3         5  
  3         200  
11 3     3   16 use Scalar::Util;
  3         4  
  3         88  
12              
13 3     3   977 use Context::Singleton::Frame::DB;
  3         7  
  3         84  
14 3     3   1015 use Context::Singleton::Exception::Invalid;
  3         8  
  3         73  
15 3     3   1021 use Context::Singleton::Exception::Deduced;
  3         7  
  3         71  
16 3     3   949 use Context::Singleton::Exception::Nondeducible;
  3         7  
  3         68  
17 3     3   964 use Context::Singleton::Frame::Promise;
  3         6  
  3         88  
18 3     3   1011 use Context::Singleton::Frame::Promise::Builder;
  3         6  
  3         70  
19 3     3   982 use Context::Singleton::Frame::Promise::Rule;
  3         6  
  3         154  
20              
21             use overload (
22 96     96   726 '""' => sub { ref ($_[0]) . '[' . $_[0]->{depth} . ']' },
23 3         23 fallback => 1,
24 3     3   17 );
  3         18  
25              
26             sub new {
27 66     66 0 58856 my ($class, %proclaim) = @_;
28 66         160 my $self = {
29             promises => {},
30             depth => 0,
31             db => $class->default_db_instance,
32             };
33              
34 66 100       162 if (ref $class) {
35 22         40 $self->{root} = $class->{root};
36 22         35 $self->{parent} = $class;
37 22         35 $self->{db} = $class->{db};
38 22         33 $self->{depth} = $class->{depth} + 1;
39              
40 22         38 $class = ref $class;
41             }
42              
43 66 100       133 unless ($self->{root}) {
44 44         64 $self->{root} = $self;
45 44         123 Scalar::Util::weaken $self->{root};
46             }
47              
48 66         116 $self = bless $self, $class;
49              
50 66         172 $self->proclaim (%proclaim);
51              
52 66         147 return $self;
53             }
54              
55             sub depth {
56 205     205 0 499 $_[0]->{depth};
57             }
58              
59             sub parent {
60 122     122 0 251 $_[0]->{parent};
61             }
62              
63             sub default_db_class {
64 100     100 0 370 'Context::Singleton::Frame::DB';
65             }
66              
67             sub default_db_instance {
68 66     66 0 146 $_[0]->default_db_class->instance;
69             }
70              
71             sub db {
72 401     401 0 1070 $_[0]->{db};
73             }
74              
75             sub debug {
76 0     0 0 0 my ($self, @message) = @_;
77              
78 0         0 my $sub = (caller(1))[3];
79 0         0 $sub =~ s/^.*://;
80              
81 3     3   983 use feature 'say';
  3         8  
  3         3208  
82 0         0 say "# [${\ $self->depth}] $sub ${\ join ' ', @message }";
  0         0  
  0         0  
83             }
84              
85             sub _build_builder_promise_for {
86 44     44   65 my ($self, $builder) = @_;
87              
88 44         79 my $promise = $self->_class_builder_promise->new (
89             depth => $self->depth,
90             builder => $builder,
91             );
92              
93 44         132 my %optional = $builder->default;
94 44         91 my %required = map +($_ => 1), $builder->required;
95 44         75 delete @required{ keys %optional };
96              
97 44         116 $promise->add_dependencies (
98             map $self->_search_promise_for ($_), keys %required
99             );
100              
101 44 100       149 $promise->set_deducible (0) unless keys %required;
102              
103             $promise->listen ($self->_search_promise_for ($_))
104 44         79 for keys %optional;
105              
106 44         102 $promise;
107             }
108              
109             sub _build_rule_promise_for {
110 88     88   144 my ($self, $rule) = @_;
111              
112 88   33     174 $self->{promises}{$rule} // do {
113 88         158 my $promise = $self->{promises}{$rule} = $self->_class_rule_promise->new (
114             depth => $self->depth,
115             rule => $rule,
116             );
117              
118 88 100       168 $promise->add_dependencies ($self->parent->_search_promise_for ($rule))
119             if $self->parent;
120              
121 88         163 for my $builder ($self->db->find_builder_for ($rule)) {
122 44         97 $promise->add_dependencies (
123             $self->_build_builder_promise_for ($builder)
124             );
125             }
126              
127 88         256 $promise;
128             };
129             }
130              
131             sub _class_builder_promise {
132 44     44   84 'Context::Singleton::Frame::Promise::Builder';
133             }
134              
135             sub _class_rule_promise {
136 88     88   154 'Context::Singleton::Frame::Promise::Rule';
137             }
138              
139             sub _deduce_rule {
140 41     41   62 my ($self, $rule) = @_;
141              
142 41         59 my $promise = $self->_search_promise_for( $rule );
143 41 100       77 return $promise->value if $promise->is_deduced;
144              
145 26         55 my $builder_promise = $promise->deducible_builder;
146 26 50       51 return $builder_promise->value if $builder_promise->is_deduced;
147              
148 26         70 my $builder = $builder_promise->builder;
149 26         52 my %deduced = $builder->default;
150              
151 26         51 for my $dependency ($builder->required) {
152             # dependencies with default values may not be deducible
153             # relying on promises to detect deducible values
154 22 100       44 next unless $self->is_deducible( $dependency );
155              
156 19         44 $deduced{$dependency} = $self->deduce ($dependency);
157             }
158              
159 26         71 $builder->build (\%deduced);
160             }
161              
162             sub _execute_triggers {
163 30     30   45 my ($self, $rule, $value) = @_;
164              
165 30         52 $_->($value) for $self->db->find_trigger_for ($rule);
166             }
167              
168             sub _find_promise_for {
169 266     266   359 my ($self, $rule) = @_;
170              
171 266         732 $self->{promises}{$rule};
172             }
173              
174             sub _frame_by_depth {
175 44     44   353 my ($self, $depth) = @_;
176              
177 44 100       96 return if $depth < 0;
178              
179 43         69 my $distance = $self->depth - $depth;
180 43 100       78 return if $distance < 0;
181              
182 42         54 my $found = $self;
183              
184 42         85 $found = $found->parent
185             while $distance-- > 0;
186              
187 42         95 $found;
188             }
189              
190             sub _root_frame {
191 2     2   199 $_[0]->{root};
192             }
193              
194             sub _search_promise_for {
195 181     181   252 my ($self, $rule) = @_;
196              
197 181   66     273 $self->_find_promise_for ($rule)
198             // $self->_build_rule_promise_for ($rule)
199             ;
200             }
201              
202             sub _set_promise_value {
203 30     30   54 my ($self, $promise, $value) = @_;
204              
205 30         48 $promise->set_value ($value, $self->depth);
206 30         60 $self->_execute_triggers ($promise->rule, $value);
207              
208 30         83 $value;
209             }
210              
211             sub _throw_deduced {
212 3     3   7 my ($self, $rule) = @_;
213              
214 3         16 throw Context::Singleton::Exception::Deduced ($rule);
215             }
216              
217             sub _throw_nondeducible {
218 3     3   8 my ($self, $rule) = @_;
219              
220 3         34 throw Context::Singleton::Exception::Nondeducible ($rule);
221             }
222              
223             sub contrive {
224 243     243 0 1554 my ($self, $rule, @how) = @_;
225              
226 243         389 $self->db->contrive ($rule, @how);
227             }
228              
229             sub load_rules {
230 6     6 0 13 shift->db->load_rules (@_);
231             }
232              
233             sub trigger {
234 0     0 0 0 shift->db->trigger (@_);
235             }
236              
237             sub deduce {
238 43     43 0 1539 my ($self, $rule, @proclaim) = @_;
239              
240 43 50       88 $self = $self->new (@proclaim) if @proclaim;
241              
242 43 100       88 $self->_throw_nondeducible ($rule)
243             unless $self->try_deduce ($rule);
244              
245 40         70 $self->_find_promise_for ($rule)->value;
246             }
247              
248             sub is_deduced {
249 12     12 0 2805 my ($self, $rule) = @_;
250              
251 12 100       27 return unless my $promise = $self->_find_promise_for ($rule);
252 7         18 return $promise->is_deduced;
253             }
254              
255             sub is_deducible {
256 29     29 0 930 my ($self, $rule) = @_;
257              
258 29 50       57 return unless my $promise = $self->_search_promise_for ($rule);
259 29         56 return $promise->is_deducible;
260             }
261              
262             sub proclaim {
263 82     82 0 1352 my ($self, @proclaim) = @_;
264              
265 82 100       179 return unless @proclaim;
266              
267 31         41 my $retval;
268 31         61 while (@proclaim) {
269 33         48 my $key = shift @proclaim;
270 33         48 my $value = shift @proclaim;
271              
272 33   66     61 my $promise = $self->_find_promise_for ($key)
273             // $self->_build_rule_promise_for ($key)
274             ;
275              
276 33 100       77 $self->_throw_deduced ($key)
277             if $promise->is_deduced;
278              
279 30         95 $retval = $self->_set_promise_value ($promise, $value);
280             }
281              
282 28         46 $retval;
283             }
284              
285             sub try_deduce {
286 48     48 0 85 my ($self, $rule) = @_;
287              
288 48         79 my $promise = $self->_search_promise_for ($rule);
289 48 100       91 return unless $promise->is_deducible;
290              
291 41         68 my $value = $self
292             ->_frame_by_depth ($promise->deduced_in_depth)
293             ->_deduce_rule ($promise->rule)
294             ;
295              
296 41         162 $promise->set_value ($value, $promise->deduced_in_depth);
297              
298 41         75 1;
299             }
300              
301             1;
302              
303             __END__