File Coverage

blib/lib/Data/MethodProxy.pm
Criterion Covered Total %
statement 57 58 98.2
branch 29 32 90.6
condition n/a
subroutine 11 11 100.0
pod 4 5 80.0
total 101 106 95.2


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