File Coverage

blib/lib/Data/MethodProxy.pm
Criterion Covered Total %
statement 55 56 98.2
branch 29 32 90.6
condition n/a
subroutine 10 10 100.0
pod 4 5 80.0
total 98 103 95.1


line stmt bran cond sub pod time code
1             package Data::MethodProxy;
2              
3             $Data::MethodProxy::VERSION = '0.03';
4              
5             =head1 NAME
6              
7             Data::MethodProxy - Inject dynamic data into static data.
8              
9             =head1 SYNOPSIS
10              
11             use Data::MethodProxy;
12            
13             my $mproxy = Data::MethodProxy->new();
14            
15             my $output = $mproxy->render({
16             half_six => ['$proxy', 'main', 'half', 6],
17             });
18             # { half_six => 3 }
19            
20             sub half {
21             my ($class, $number) = @_;
22             return $number / 2;
23             }
24              
25             =head1 DESCRIPTION
26              
27             A method proxy is an array ref describing a class method to call and the
28             arguments to pass to it. The first value of the array ref is the scalar
29             C<$proxy>, followed by a package name, then a subroutine name which must
30             callable in the package, and a list of any subroutine arguments.
31              
32             [ '$proxy', 'Foo::Bar', 'baz', 123, 4 ]
33              
34             The above is saying, do this:
35              
36             Foo::Bar->baz( 123, 4 );
37              
38             The L method is the main entry point for replacing all found
39             method proxies in an arbitrary data structure with the return value of
40             calling the methods.
41              
42             =head2 Example
43              
44             Consider this static YAML configuration:
45              
46             ---
47             db:
48             dsn: DBI:mysql:database=foo
49             username: bar
50             password: abc123
51              
52             Putting your database password inside of a configuration file is usually
53             considered a bad practice. You can use a method proxy to get around this
54             without jumping through a bunch of hoops:
55              
56             ---
57             db:
58             dsn: DBI:mysql:database=foo
59             username: bar
60             password:
61             - $proxy
62             - MyApp::Config
63             - get_db_password
64             - foo-bar
65              
66             When L is called on the above data structure it will
67             see the method proxy and will replace the array ref with the
68             return value of calling the method.
69              
70             A method proxy, in Perl syntax, looks like this:
71              
72             ['$proxy', $package, $method, @args]
73              
74             The C<$proxy> string can also be written as C<&proxy>. The above is then
75             converted to a method call and replaced by the return value of the method call:
76              
77             $package->$method( @args );
78              
79             In the above database password example the method call would be this:
80              
81             MyApp::Config->get_db_password( 'foo-bar' );
82              
83             You'd still need to create a C package, and add a
84             C method to it.
85              
86             =cut
87              
88 2     2   207899 use strict;
  2         5  
  2         56  
89 2     2   10 use warnings;
  2         4  
  2         53  
90              
91 2     2   10 use Scalar::Util qw( refaddr );
  2         4  
  2         138  
92 2     2   1225 use Module::Runtime qw( require_module is_module_name );
  2         3815  
  2         13  
93 2     2   229 use Carp qw( croak );
  2         7  
  2         1042  
94              
95             sub new {
96 2     2 0 92 my $class = shift;
97 2         5 my $self = bless {}, $class;
98 2         7 return $self;
99             }
100              
101             our $FOUND_DATA;
102              
103             =head1 METHODS
104              
105             =head2 render
106              
107             my $output = $mproxy->render( $input );
108              
109             Traverses the supplied data looking for method proxies, calling them, and
110             replacing them with the return value of the method call. Any value may be
111             passed, such as a hash ref, an array ref, a method proxy, an object, a scalar,
112             etc. Array and hash refs will be recursively searched for method proxies.
113              
114             If a circular reference is detected an error will be thrown.
115              
116             =cut
117              
118             sub render {
119 17     17 1 2796 my ($self, $data) = @_;
120              
121 17 100       64 return $data if !ref $data;
122              
123 15 100       33 local $FOUND_DATA = {} if !$FOUND_DATA;
124              
125 15         53 my $refaddr = refaddr( $data );
126 15 100       52 if ($FOUND_DATA->{$refaddr}) {
127 1         3 local $Carp::Internal{ (__PACKAGE__) } = 1;
128 1         73 croak 'Circular reference detected in data passed to render()';
129             }
130 14         30 $FOUND_DATA->{$refaddr} = 1;
131              
132 14 100       44 if (ref($data) eq 'HASH') {
    50          
133             return {
134 6         21 map { $_ => $self->render( $data->{$_} ) }
  8         38  
135             keys( %$data )
136             };
137             }
138             elsif (ref($data) eq 'ARRAY') {
139 8 100       25 if ($self->is_valid( $data )) {
140 6         16 return $self->call( $data );
141             }
142              
143             return [
144 2         4 map { $self->render( $_ ) }
  4         15  
145             @$data
146             ];
147             }
148              
149 0         0 return $data;
150             }
151              
152             =head2 call
153              
154             my $return = $mproxy->call( ['$proxy', $package, $method, @args] );
155              
156             Calls the method proxy and returns its return.
157              
158             =cut
159              
160             sub call {
161 14     14 1 4199 my ($self, $proxy) = @_;
162              
163             {
164 14         20 local $Carp::Internal{ (__PACKAGE__) } = 1;
  14         36  
165 14 100       28 croak 'Invalid method proxy passed to call()' if !$self->is_valid( $proxy );
166 10 100       24 croak 'Uncallable method proxy passed to call()' if !$self->is_callable( $proxy );
167             }
168              
169 6         20 my ($marker, $package, $method, @args) = @$proxy;
170 6         21 require_module( $package );
171 6         160 return $package->$method( @args );
172             }
173              
174             =head2 is_valid
175              
176             die unless $mproxy->is_valid( ... );
177              
178             Returns true if the passed value looks like a method proxy.
179              
180             =cut
181              
182             sub is_valid {
183 45     45 1 8753 my ($self, $proxy) = @_;
184              
185 45 100       122 return 0 if ref($proxy) ne 'ARRAY';
186 40         90 my ($marker, $package, $method, @args) = @$proxy;
187              
188 40 100       408 return 0 if !defined $marker;
189 37 100       139 return 0 if $marker !~ m{^[&\$]proxy$};
190 34 100       151 return 0 if !defined $package;
191 32 100       122 return 0 if !defined $method;
192              
193 31         88 return 1;
194             }
195              
196             =head2 is_callable
197              
198             die unless $mproxy->is_callable( ... );
199              
200             Returns true if the passed value looks like a method proxy,
201             and has a package and method which exist.
202              
203             =cut
204              
205             sub is_callable {
206 13     13 1 3695 my ($self, $proxy) = @_;
207              
208 13 50       116 return 0 if !$self->is_valid( $proxy );
209 13         30 my ($marker, $package, $method, @args) = @$proxy;
210              
211 13 50       35 return 0 if !is_module_name( $package );
212 13 100       662 return 0 if !$package->can( $method );
213              
214 7         27 return 1;
215             }
216              
217             1;
218             __END__