File Coverage

blib/lib/Salvation/Method/Signatures.pm
Criterion Covered Total %
statement 92 104 88.4
branch 17 22 77.2
condition 0 4 0.0
subroutine 15 16 93.7
pod 6 6 100.0
total 130 152 85.5


line stmt bran cond sub pod time code
1             package Salvation::Method::Signatures;
2              
3             =head1 NAME
4              
5             Salvation::Method::Signatures - Реализация сигнатур для методов
6              
7             =head1 SYNOPSIS
8              
9             package Some::Package;
10              
11             use Salvation::Method::Signatures;
12             # use Test::More tests => 3;
13              
14             method process( ArrayRef[ArrayRef[Str]] :flags!, ArrayRef[HashRef(Int :id!)] :data! ) {
15              
16             # isa_ok( $self, 'Some::Package' );
17             # is_deeply( $flags, [ [ 'something' ] ] );
18             # is_deeply( $data, [ { id => 1 } ] );
19              
20             ...
21             }
22              
23             package main;
24              
25             Some::Package -> process(
26             flags => [ [ 'something' ] ],
27             data => [ { id => 1 } ],
28             );
29              
30             =head1 DESCRIPTION
31              
32             Делает то же, что делают другие реализации сигнатур: проверяет тип аргументов
33             метода, само разбирает C<@_> и инжектит переменные в блок.
34              
35             =head1 SEE ALSO
36              
37             http://perlcabal.org/syn/S06.html#Signatures
38             L
39             L
40              
41             =cut
42              
43 1     1   594 use strict;
  1         2  
  1         40  
44 1     1   6 use warnings;
  1         2  
  1         34  
45 1     1   579 use boolean;
  1         4121  
  1         5  
46              
47 1     1   90 use B ();
  1         2  
  1         27  
48 1     1   686 use Module::Load 'load';
  1         1337  
  1         7  
49 1     1   655 use Salvation::UpdateGvFLAGS ();
  1         906  
  1         35  
50              
51 1     1   7 use base 'Devel::Declare::MethodInstaller::Simple';
  1         2  
  1         718  
52              
53             our $VERSION = 0.04;
54              
55             =head1 METHODS
56              
57             =cut
58              
59             =head2 type_system_class()
60              
61             =cut
62              
63             sub type_system_class {
64              
65 8     8 1 23 return 'Salvation::TC';
66             }
67              
68             =head2 token_str()
69              
70             =cut
71              
72             sub token_str {
73              
74 1     1 1 7 return 'method';
75             }
76              
77             =head2 self_var_name()
78              
79             =cut
80              
81             sub self_var_name {
82              
83 8     8 1 12 return '$self';
84             }
85              
86             =head2 import()
87              
88             Экспортирует магическое ключевое слово.
89              
90             Подробнее: L.
91              
92             =cut
93              
94             sub import {
95              
96 1     1   8 my ( $self ) = @_;
97 1         2 my $caller = caller();
98              
99 1         3 $self -> install_methodhandler(
100             name => $self -> token_str(),
101             into => $caller,
102             );
103              
104 1         224 return;
105             }
106              
107             {
108             my %installed_methods = ();
109              
110             =head2 strip_name()
111              
112             Обёртка вокруг L.
113              
114             Делает всё то же самое, но дополнительно запоминает, в каком модуле какие
115             методы были объявлены с использованием L.
116              
117             Внутренний метод.
118              
119             =cut
120              
121             sub strip_name {
122              
123 8     8 1 17699 my ( $self, @rest ) = @_;
124 8         30 my $name = $self -> SUPER::strip_name( @rest );
125              
126 8         171 push( @{ $installed_methods{ $self -> { 'into' } } }, $name );
  8         17  
127              
128 8         20 return $name;
129             }
130              
131             =head2 mark_methods_as_not_imported( Str class )
132              
133             Маркирует все методы класса C, объявленные с использованием
134             L, как "не импортированные".
135              
136             Внутренний метод.
137              
138             =cut
139              
140             sub mark_methods_as_not_imported {
141              
142 0     0 1 0 my ( $self, $class ) = @_;
143 0   0     0 my $imported_cv = ( eval { B::GVf_IMPORTED_CV() } || 0x80 );
144              
145 1     1   23182 no strict 'refs';
  1         2  
  1         661  
146              
147 0   0     0 foreach my $method ( @{ $installed_methods{ $class } // [] } ) {
  0         0  
148              
149 0         0 my $name = "${class}::${method}";
150 0         0 my $obj = B::svref_2object( \*$name );
151              
152 0 0       0 if( $obj -> GvFLAGS() & $imported_cv ) {
153              
154 0         0 Salvation::UpdateGvFLAGS::toggle_glob_flag_by_name( $name, $imported_cv );
155             }
156             }
157              
158 0         0 return;
159             }
160             }
161              
162             =head2 parse_proto( Str $str )
163              
164             Разбирает прототип метода, генерирует код и инжектит этот код в метод.
165              
166             Подробнее: L.
167              
168             =cut
169              
170             sub parse_proto {
171              
172 8     8 1 317 my ( $self, $str ) = @_;
173 8         28 load my $type_system_class = $self -> type_system_class();
174 8         87507 my $type_parser = $type_system_class -> get_type_parser();
175 8 100       67 my $sig = ( ( $str =~ m/^\s*$/ )
176             ? { data => [], opts => {} }
177             : $type_parser -> tokenize_signature_str( "(${str})", {} )
178             );
179 8         166 ( $sig, my $opts ) = @$sig{ 'data', 'opts' };
180              
181 8         21 my @positional_vars = ( $self -> self_var_name() );
182 8         9 my $code = '';
183 8         8 my $pos = 0;
184 8         20 my $prev_was_optional = false;
185              
186             my $wrap_check = sub {
187              
188 15     15   16 my ( $code, $param_name ) = @_;
189              
190 15         35 return sprintf(
191             '( eval{ local $Carp::CarpLevel = 2; %s } || die( "Validation for parameter \"%s\" failed because:\n$@" ) )',
192             $code,
193             $param_name,
194             );
195 8         35 };
196              
197 8         27 while( defined( my $item = shift( @$sig ) ) ) {
198              
199 13 100       27 if( $item -> { 'param' } -> { 'named' } ) {
200              
201 3 50       6 if( $prev_was_optional ) {
202              
203 0         0 die( "Error at signature (${str}): named parameter can't follow optional positional parameter" );
204             }
205              
206 3         18 unshift( @$sig, $item );
207 3         4 last;
208             }
209              
210 10         20 my $type = $type_system_class -> materialize_type( $item -> { 'type' } );
211 10         138 my $arg = $item -> { 'param' };
212              
213 10         16 my $var = sprintf( '$%s', $arg -> { 'name' } );
214              
215 10         11 push( @positional_vars, $var );
216              
217 10         26 my $check = sprintf( '%s -> assert( %s, \'%s\' )', $type_system_class, $var, $type -> name() );
218              
219 10         41 $check = $wrap_check -> ( $check, $arg -> { 'name' } );
220              
221 10 100       71 if( $arg -> { 'optional' } ) {
    50          
222              
223 3         5 $prev_was_optional = true;
224              
225 3         11 $check = sprintf( '( ( scalar( @_ ) > %d ) ? %s : 1 )', 1 + $pos, $check );
226              
227             } elsif( $prev_was_optional ) {
228              
229 0         0 die( "Error at signature (${str}): required positional parameter can't follow optional one" );
230             }
231              
232 10         58 $code .= $check;
233 10         9 $code .= ';';
234              
235 10         19 $type_system_class -> get( $type -> name() ); # прогрев кэша
236              
237 10         138 ++$pos;
238             }
239              
240 8         10 my @named_vars = ();
241 8         6 my @named_params = ();
242 8         9 my $named_checks = '';
243              
244 8         16 while( defined( my $item = shift( @$sig ) ) ) {
245              
246 5 50       10 if( $item -> { 'param' } -> { 'positional' } ) {
247              
248 0         0 die( "Error at signature (${str}): positional parameter can't follow named parameter" );
249             }
250              
251 5         12 my $type = $type_system_class -> materialize_type( $item -> { 'type' } );
252 5         82 my $arg = $item -> { 'param' };
253              
254 5         11 push( @named_vars, sprintf( '$%s', $arg -> { 'name' } ) );
255 5         8 push( @named_params, sprintf( '\'%s\'', $arg -> { 'name' } ) );
256              
257 5         13 my $check = sprintf( '%s -> assert( $args{ \'%s\' }, \'%s\' )', $type_system_class, $arg -> { 'name' }, $type -> name() );
258              
259 5         20 $check = $wrap_check -> ( $check, $arg -> { 'name' } );
260              
261 5 100       11 if( $arg -> { 'optional' } ) {
262              
263 3         6 $prev_was_optional = true;
264              
265 3         13 $check = sprintf( '( exists( $args{ \'%s\' } ) ? %s : 1 )', $arg -> { 'name' }, $check );
266             }
267              
268 5         7 $named_checks .= $check;
269 5         22 $named_checks .= ';';
270             }
271              
272 8         8 my $named_vars_code = '';
273              
274 8 100       18 if( $named_checks ) {
    100          
275              
276 3 100       7 if( $opts -> { 'strict' } ) {
277              
278 1         13 $named_vars_code = sprintf( '( my ( %s ) = do {
279              
280             no warnings \'syntax\';
281              
282             if( scalar( () = @_[ %d .. $#_ ] ) %% 2 ) {
283             die( "Too many positional parameters" );
284             }
285             my %%args = @_[ %d .. $#_ ]; %s my @l = delete( @args{ %s } );
286             if( scalar( keys( %%args ) ) > 0 ) {
287             die( "Unexpected named parameters found: " . join( ", ", keys( %%args ) ) );
288             }
289             @l;
290              
291             } );', join( ', ', @named_vars ), ( scalar( @positional_vars ) )x2, $named_checks, join( ', ', @named_params ) );
292              
293             } else {
294              
295 2         15 $named_vars_code = sprintf( '( my ( %s ) = do {
296              
297             no warnings \'syntax\';
298              
299             if( scalar( () = @_[ %d .. $#_ ] ) %% 2 ) {
300             die( "Too many positional parameters" );
301             }
302             my %%args = @_[ %d .. $#_ ]; %s @args{ %s };
303              
304             } );', join( ', ', @named_vars ), ( scalar( @positional_vars ) )x2, $named_checks, join( ', ', @named_params ) );
305             }
306              
307             } elsif( $opts -> { 'strict' } ) {
308              
309 1         2 $named_vars_code = sprintf( 'if( scalar( @_ ) > %d ) {
310             die( "Too many positional parameters" );
311             }', scalar( @positional_vars ) );
312             }
313              
314 8         24 $code = sprintf( 'my ( %s ) = @_; %s %s local @_ = ();', join( ', ', @positional_vars ), $code, $named_vars_code );
315              
316 8         41 $code =~ s/\n/ /g;
317 8         112 $code =~ s/\s{2,}/ /g;
318              
319 8         48 return $code;
320             }
321              
322             1;
323              
324             __END__