File Coverage

lib/Context/Singleton/Frame.pm
Criterion Covered Total %
statement 139 146 95.2
branch 29 32 90.6
condition 5 9 55.5
subroutine 40 42 95.2
pod 0 15 0.0
total 213 244 87.3


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