File Coverage

blib/lib/Type/Tie/Aggregate/Deep.pm
Criterion Covered Total %
statement 75 76 98.6
branch 19 24 79.1
condition 6 9 66.6
subroutine 20 20 100.0
pod 1 2 50.0
total 121 131 92.3


line stmt bran cond sub pod time code
1             # ABSTRACT: used to deeply tie variables
2              
3             ######################################################################
4             # Copyright (C) 2021 Asher Gordon #
5             # #
6             # This program is free software: you can redistribute it and/or #
7             # modify it under the terms of the GNU General Public License as #
8             # published by the Free Software Foundation, either version 3 of #
9             # the License, or (at your option) any later version. #
10             # #
11             # This program is distributed in the hope that it will be useful, #
12             # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
14             # General Public License for more details. #
15             # #
16             # You should have received a copy of the GNU General Public License #
17             # along with this program. If not, see #
18             # . #
19             ######################################################################
20              
21             package Type::Tie::Aggregate::Deep;
22             $Type::Tie::Aggregate::Deep::VERSION = '0.001';
23             #pod =head1 DESCRIPTION
24             #pod
25             #pod This package contains the C function, used to deeply tie
26             #pod references. It also contains several other packages used to tie
27             #pod deeply.
28             #pod
29             #pod =cut
30              
31 5     5   94 use v5.18.0;
  5         19  
32 5     5   31 use strict;
  5         10  
  5         117  
33 5     5   28 use warnings;
  5         9  
  5         173  
34 5     5   32 use feature 'lexical_subs';
  5         12  
  5         693  
35 5     5   35 no warnings 'experimental::lexical_subs';
  5         11  
  5         232  
36 5     5   29 use namespace::autoclean;
  5         12  
  5         64  
37 5     5   408 use Carp;
  5         11  
  5         402  
38 5     5   35 use Scalar::Util qw(blessed);
  5         11  
  5         268  
39             # For _check_ref_type() and _tied_types().
40 5     5   35 use parent 'Type::Tie::Aggregate';
  5         41  
  5         61  
41              
42             our @CARP_NOT = qw(Type::Tie::Aggregate::Base);
43              
44             sub reftype ($) {
45 363     363 0 914 my $type = Scalar::Util::reftype $_[0];
46 363 100 66     1276 return 'SCALAR' if defined $type && $type eq 'REF';
47 357         822 return $type;
48             }
49              
50             my %get_children = (
51             SCALAR => sub { $$_ },
52             ARRAY => sub { @$_ },
53             HASH => sub { values %$_ },
54             );
55              
56             # TODO: Handle circular references correctly.
57             my sub children {
58             map {
59 50     50   106 my $type = reftype $_;
  50         140  
60 50 50       155 my $get = $get_children{$type}
61             or confess "Invalid reference type: $type";
62 50         192 $get->($_)
63             } @_;
64             }
65              
66             my $package_name = sub {
67             my ($class, $type, $tied) = @_;
68             $class .= '::Tied' if $tied;
69             $class .= '::' . ucfirst lc $type;
70             return $class;
71             };
72              
73             my sub tie_deeply;
74             sub tie_deeply {
75 50     50   146 my $obj = shift;
76             # Don't tie blessed refs, because we want to preserve
77             # encapsulation.
78 50 100       108 my @refs = grep { ref ne '' && ! defined blessed $_ } @_;
  70         300  
79              
80 50         228 foreach my $ref (@refs) {
81 18 50       113 my $type = __PACKAGE__->_check_ref_type($ref) or next;
82 18         84 my $tied = &CORE::tied($ref);
83 18 100       72 my $tied_type = blessed $tied if $tied;
84              
85 18         39 my $pkg = __PACKAGE__;
86              
87             # Don't tie deeply if it's already tied deeply.
88 18 100 100     164 if (defined $tied_type && $tied_type =~ /^\Q$pkg\E::/) {
89 8 100       28 if ($tied->object != $obj) {
90 1         4 my ($old, $new) =
91             map $_->_type, $tied->object, $obj;
92 1         11 croak "Cannot tie $ref to $new; already tied to $old";
93             }
94             }
95             else {
96 10         33 $pkg = __PACKAGE__->$package_name($type, $tied);
97              
98 10 50       65 my @args = (
    100          
    50          
99             $type eq 'SCALAR' ? $$ref :
100             $type eq 'ARRAY' ? @$ref :
101             $type eq 'HASH' ? %$ref :
102             die "Invalid type: $type",
103             );
104              
105 10         91 my %params = (object => $obj);
106 10 100       27 $params{ref} = $tied if $tied;
107 10         137 &CORE::tie($ref, $pkg, \%params, @args);
108             }
109              
110 17         51 tie_deeply $obj, children $ref;
111             }
112             }
113              
114             #pod =func deep_tie
115             #pod
116             #pod deep_tie $obj, $ref;
117             #pod
118             #pod Tie C<$ref> to C<$obj> deeply, so that whenever C<$ref> changes,
119             #pod C<< $obj->_check >> will be called. If C<$ref> is not defined, it
120             #pod defaults to C<< $obj->_ref >>.
121             #pod
122             #pod Currently this does not handle circular references correctly. See
123             #pod L.
124             #pod
125             #pod =cut
126              
127             sub deep_tie {
128 237     237 1 451 my ($obj, $ref) = @_;
129 237   33     806 $ref //= $obj->_ref;
130 237         508 tie_deeply $obj, children $ref;
131             }
132              
133             # How to initialize objects which tie various types.
134             my %tie_initializers = (
135             SCALAR => sub { $_[0]->STORE($_[1]) },
136             ARRAY => sub { my $s = shift; $s->CLEAR; $s->PUSH(@_) },
137             HASH => sub {
138             my $self = shift;
139             carp 'Odd number of elements in hash initialization'
140             if @_ % 2;
141             $self->CLEAR;
142             while (my ($key, $value) = splice @_, 0, 2) {
143             $self->STORE($key => $value);
144             }
145             }
146             );
147              
148             # Initialize the packages.
149             foreach my $type (__PACKAGE__->_tied_types) {
150             my @parents = (Type::Tie::Aggregate->$package_name($type));
151             require s|::|/|gr . '.pm' foreach @parents;
152              
153             foreach my $tied (0, 1) {
154             my $class = __PACKAGE__->$package_name($type, $tied);
155              
156             my %pkg_globs = (
157             VERSION => our $VERSION,
158             ISA => [
159             __PACKAGE__->$package_name('Base', $tied), @parents,
160             ],
161             );
162              
163             if ($tied) {
164             # Make the constructor initialize elements on the
165             # underlying tied object.
166             my $initialize = $tie_initializers{$type}
167             or die "Invalid type: $type";
168             $pkg_globs{_initialize} = sub {
169 100     100   151 my $self = shift;
170 100         234 $self->_ref->$initialize(@_);
171             };
172             }
173              
174             while (my ($name, $value) = each %pkg_globs) {
175             $name = "$class\::$name";
176 5     5   7167 no strict 'refs';
  5         13  
  5         673  
177             (ref $value eq '' ? $$name : *$name) = $value;
178             }
179             }
180             }
181              
182             package Type::Tie::Aggregate::Deep::Base {
183             $Type::Tie::Aggregate::Deep::Base::VERSION = '0.001';
184 5     5   39 use parent 'Type::Tie::Aggregate::Base';
  5         11  
  5         23  
185              
186             sub _new {
187 114     114   236 my $class = shift;
188 114         167 my %params = %{+shift};
  114         487  
189 114         272 my $self = bless \%params, $class;
190 114         440 $self->_initialize(@_);
191 15         68 return $self;
192             }
193              
194             # The user should never see this class anyway, so no need to name
195             # the accessor _object().
196             sub object {
197 128     128   244 my $self = shift;
198 128 50       490 return $self->{object} unless @_;
199 0         0 ($self->{object}) = @_;
200             }
201              
202             sub _check_and_retie {
203 15     15   42 my $self = shift;
204 15         92 $self->object->_check_and_retie(@_);
205             }
206             }
207              
208             package Type::Tie::Aggregate::Deep::Tied::Base {
209             $Type::Tie::Aggregate::Deep::Tied::Base::VERSION = '0.001';
210 5     5   1461 use parent -norequire => 'Type::Tie::Aggregate::Deep::Base';
  5         13  
  5         44  
211              
212             # This class provides methods which fall back to the tied object
213             # in $ref.
214              
215             # Methods to install.
216             my @install_methods = (
217             [
218             { mutates => 1 }, qw(
219             STORESIZE STORE DELETE CLEAR POP PUSH SHIFT UNSHIFT
220             ),
221             ],
222             [
223             { mutates => 0 }, qw(
224             FETCHSIZE FETCH FIRSTKEY NEXTKEY EXISTS SCALAR
225             ),
226             ],
227             );
228              
229             foreach my $methods (@install_methods) {
230             my ($params, @methods) = @$methods;
231             @$methods = (
232             $params, map { $_ => "\$ref->$_(\@_)" } @methods,
233             );
234             }
235              
236             __PACKAGE__->_install_methods(@$_) foreach @install_methods;
237             }
238              
239             #pod =head1 SEE ALSO
240             #pod
241             #pod =for :list
242             #pod * L
243             #pod
244             #pod =cut
245              
246             1;
247              
248             __END__