File Coverage

blib/lib/Ryu/Observable.pm
Criterion Covered Total %
statement 70 96 72.9
branch 11 18 61.1
condition 1 6 16.6
subroutine 23 35 65.7
pod 14 14 100.0
total 119 169 70.4


line stmt bran cond sub pod time code
1             package Ryu::Observable;
2              
3 2     2   342486 use strict;
  2         4  
  2         65  
4 2     2   6 use warnings;
  2         6  
  2         99  
5              
6 2     2   983 use utf8;
  2         1852  
  2         12  
7              
8             our $VERSION = '4.001'; # VERSION
9             our $AUTHORITY = 'cpan:TEAM'; # AUTHORITY
10              
11             =encoding utf8
12              
13             =head1 NAME
14              
15             Ryu::Observable - plus ça change
16              
17             =head1 SYNOPSIS
18              
19             # Set initial value
20             my $observed = Ryu::Observable->new(123)
21             # and a callback for any changes
22             ->subscribe(sub { print "New value, is now: $_\n" });
23             # Basic numeric increment/decrement should trigger a notification
24             ++$observed;
25             # To assign a new value, use ->set_numeric or ->set_string
26             $observed->set_numeric(88);
27              
28             =head1 DESCRIPTION
29              
30             Simple monitorable variables.
31              
32             =cut
33              
34             use overload
35 1     1   7 '""' => sub { shift->as_string },
36 1     1   50 '0+' => sub { shift->as_number },
37 1     1   8 '++' => sub { my $v = ++$_[0]->{value}; $_[0]->notify_all; $v },
  1         6  
  1         3  
38 0     0   0 '--' => sub { my $v = --$_[0]->{value}; $_[0]->notify_all; $v },
  0         0  
  0         0  
39 7     7   44 'bool' => sub { !!shift->{value} },
40 2     2   1011 fallback => 1;
  2         1566  
  2         29  
41              
42 2     2   269 use Scalar::Util;
  2         3  
  2         130  
43 2     2   1091 use List::UtilsBy;
  2         3894  
  2         153  
44              
45 2     2   1246 use Ryu::Source;
  2         10  
  2         327  
46              
47             # Slightly odd way of applying this - we don't want to require Sentinel,
48             # but the usual tricks of ->import or using *Sentinel::sentinel directly
49             # only work for the pure-perl version. So, we try to load it, making the
50             # syntax available, and we then use sentinel() as if it were a function...
51             # providing a fallback *sentinel only when the load failed.
52             BEGIN {
53             eval {
54 2         903 require Sentinel;
55 0         0 Sentinel->import;
56 0         0 1
57 2 50   2   8 } or do {
58 2     0   2740 *sentinel = sub { die 'This requires the Sentinel module to be installed' };
  0         0  
59             }
60             }
61              
62             =head1 METHODS
63              
64             Public API, such as it is.
65              
66             =head2 as_string
67              
68             Returns the string representation of this value.
69              
70             =cut
71              
72 1     1 1 9 sub as_string { '' . shift->{value} }
73              
74             =head2 as_number
75              
76             =head2 as_numeric
77              
78             Returns the numeric representation of this value.
79              
80             (this method is available as C or C, both operate the same way)
81              
82             =cut
83              
84 1     1 1 9 sub as_number { 0 + shift->{value} }
85              
86             *as_numeric = *as_number;
87              
88             =head2 new
89              
90             Instantiates with the given value.
91              
92             my $observed = Ryu::Observable->new('whatever');
93              
94             =cut
95              
96 8     8 1 259553 sub new { bless { value => $_[1] }, $_[0] }
97              
98             =head2 subscribe
99              
100             Requests notifications when the value changes.
101              
102             my $observed = Ryu::Observable->new('whatever')
103             ->subscribe(sub { print "New value - $_\n" });
104              
105             =cut
106              
107 1     1 1 12 sub subscribe { my $self = shift; push @{$self->{subscriptions}}, @_; $self }
  1         3  
  1         4  
  1         4  
108              
109             =head2 unsubscribe
110              
111             Removes an existing callback.
112              
113             my $code;
114             my $observed = Ryu::Observable->new('whatever')
115             ->subscribe($code = sub { print "New value - $_\n" })
116             ->set_string('test')
117             ->unsubscribe($code);
118              
119             =cut
120              
121             sub unsubscribe {
122 0     0 1 0 my ($self, @code) = @_;
123 0         0 for my $addr (map Scalar::Util::refaddr($_), @code) {
124 0     0   0 List::UtilsBy::extract_by { Scalar::Util::refaddr($_) == $addr } @{$self->{subscriptions}};
  0         0  
  0         0  
125             }
126             $self
127 0         0 }
128              
129             =head2 set
130              
131             Sets the value to the given scalar, then notifies all subscribers (regardless
132             of whether the value has changed or not).
133              
134             =cut
135              
136 1     1 1 558 sub set { my ($self, $v) = @_; $self->{value} = $v; $self->notify_all }
  1         4  
  1         5  
137              
138             =head2 value
139              
140             Returns the raw value.
141              
142             =cut
143              
144 0     0 1 0 sub value { shift->{value} }
145              
146             =head2 set_numeric
147              
148             =head2 set_number
149              
150             Applies a new numeric value, and notifies subscribers if the value is numerically
151             different to the previous one (or if we had no previous value).
152              
153             Returns C<$self>.
154              
155             (this method is available as C or C, both operate the same way)
156              
157             =cut
158              
159             sub set_numeric {
160 0     0 1 0 my ($self, $v) = @_;
161 0         0 my $prev = $self->{value};
162 0 0 0     0 return $self if defined($prev) && $prev == $v;
163 0         0 $self->{value} = $v;
164 0         0 $self->notify_all
165             }
166              
167             *set_number = *set_numeric;
168              
169             =head2 set_string
170              
171             Applies a new string value, and notifies subscribers if the value stringifies to a
172             different value than the previous one (or if we had no previous value).
173              
174             Returns C<$self>.
175              
176             =cut
177              
178             sub set_string {
179 1     1 1 4 my ($self, $v) = @_;
180 1         5 my $prev = $self->{value};
181 1 50 33     30 return $self if defined($prev) && $prev eq $v;
182 1         4 $self->{value} = $v;
183 1         6 $self->notify_all
184             }
185              
186             =head2 source
187              
188             Returns a L, which will emit each new value
189             until the observable is destroyed.
190              
191             =cut
192              
193             sub source {
194 2     2 1 10 my ($self) = @_;
195 2 100       14 return $self->{source} if $self->{source};
196 1         31 $self->{source} = my $src = Ryu::Source->new;
197 1         3 Scalar::Util::weaken(my $copy = $self);
198             $src->_completed->on_ready(sub {
199 1 50   1   32 delete $copy->{source} if $copy
200 1         10 });
201 1         28 $src;
202             }
203              
204             =head1 LVALUE METHODS
205              
206             B<< These require L to be installed >>.
207              
208             =head2 lvalue_str
209              
210             Returns a L lvalue accessor for the string value.
211              
212             This can be used with refaliasing or C loops to reduce typing:
213              
214             for($observable->lvalue_str) {
215             chomp;
216             s/_/-/g;
217             }
218              
219             Any attempt to retrieve or set the value will be redirected to L
220             or L as appropriate.
221              
222             =cut
223              
224             sub lvalue_str : lvalue {
225 0     0 1 0 my ($self) = @_;
226             sentinel(get => sub {
227 0     0   0 return $self->as_string(shift);
228             }, set => sub {
229 0     0   0 return $self->set_string(shift);
230 0         0 });
231             }
232              
233             =head2 lvalue_num
234              
235             Returns a L lvalue accessor for the numeric value.
236              
237             This can be used with refaliasing or C loops to reduce typing:
238              
239             for($observable->lvalue_num) {
240             ++$_;
241             $_ *= 3;
242             }
243              
244             Any attempt to retrieve or set the value will be redirected to L
245             or L as appropriate.
246              
247             =cut
248              
249             sub lvalue_num : lvalue {
250 0     0 1 0 my ($self) = @_;
251             sentinel(get => sub {
252 0     0   0 return $self->as_number(shift);
253             }, set => sub {
254 0     0   0 return $self->set_number(shift);
255 0         0 });
256             }
257              
258             =head2 finish
259              
260             Mark this observable as finished.
261              
262             No further events or notifications will be sent.
263              
264             =cut
265              
266             sub finish {
267 8     8 1 42 my $self = shift;
268 8         16 @{$self->{subscriptions}} = ();
  8         26  
269 8 100       30 if(my $src = $self->{source}) {
270 1 50       10 $src->finish unless $src->is_ready;
271             }
272 8         15 return $self;
273             }
274              
275             =head1 METHODS - Internal
276              
277             Don't use these.
278              
279             =head2 notify_all
280              
281             Notifies all currently-subscribed callbacks with the current value.
282              
283             =cut
284              
285             sub notify_all {
286 3     3 1 7 my $self = shift;
287 3         8 my $v = $self->{value};
288 3 100       18 $self->{source}->emit($v) if $self->{source};
289 3         8 for my $sub (@{$self->{subscriptions}}) {
  3         10  
290 2         9 $sub->($_) for $v;
291             }
292             $self
293 3         4237 }
294              
295             sub DESTROY {
296 8     8   5392 my ($self) = @_;
297 8 50       37 return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
298 8         34 $self->finish;
299 8         19 delete $self->{value};
300 8         43 return;
301             }
302              
303             1;
304              
305             __END__