Revision 963330cc
Von Sven Schöling vor mehr als 7 Jahren hinzugefügt
SL/Helper/Object.pm | ||
---|---|---|
1 |
package SL::Helper::Object; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
sub import { |
|
6 |
my ($class, @args) = @_; |
|
7 |
|
|
8 |
my $caller = (caller)[0]; |
|
9 |
|
|
10 |
while (@args > 1) { |
|
11 |
my $method = shift @args; |
|
12 |
my $args = shift @args; |
|
13 |
die "invalid method '$method' for $class" unless $class->can($method); |
|
14 |
$class->$method($caller, $args); |
|
15 |
} |
|
16 |
} |
|
17 |
|
|
18 |
my %args_string_by_key = ( |
|
19 |
none => '', |
|
20 |
raw => '(@_)', |
|
21 |
standard => '(@_[1..$#_])', |
|
22 |
); |
|
23 |
|
|
24 |
my %pre_context_by_key = ( |
|
25 |
void => '', |
|
26 |
scalar => 'my $return =', |
|
27 |
list => 'my @return =', |
|
28 |
); |
|
29 |
|
|
30 |
my %post_context_by_key = ( |
|
31 |
void => 'return', |
|
32 |
scalar => '$return', |
|
33 |
list => '@return', |
|
34 |
); |
|
35 |
|
|
36 |
my %known_delegate_args = map { $_ => 1 } qw(target_method args force_context class_function); |
|
37 |
|
|
38 |
my $_ident = '^[a-zA-Z0-9_]+$'; |
|
39 |
my $_cident = '^[a-zA-Z0-9_:]+$'; |
|
40 |
|
|
41 |
sub delegate { |
|
42 |
my ($class, $caller, $args) = @_; |
|
43 |
|
|
44 |
die 'delegate needs an array ref of parameters' if 'ARRAY' ne ref $args; |
|
45 |
die 'delegate needs an even number of args' if @$args % 2; |
|
46 |
|
|
47 |
while (@$args > 1) { |
|
48 |
my $target = shift @$args; |
|
49 |
my $delegate_args = shift @$args; |
|
50 |
my $params = 'HASH' eq ref $delegate_args->[0] ? $delegate_args->[0] : {}; |
|
51 |
|
|
52 |
$known_delegate_args{$_} || die "unknown parameter '$_'" for keys %$params; |
|
53 |
|
|
54 |
die "delegate: target '$target' must match /$_cident/" if $target !~ /$_cident/; |
|
55 |
die "delegate: target_method '$params->{target_method}' must match /$_ident/" if $params->{target_method} && $params->{target_method} !~ /$_ident/; |
|
56 |
|
|
57 |
my $method_joiner = $params->{class_function} ? '::' : '->'; |
|
58 |
|
|
59 |
for my $method (@$delegate_args) { |
|
60 |
next if ref $method; |
|
61 |
|
|
62 |
die "delegate: method name '$method' must match /$_ident/" if $method !~ /$_ident/; |
|
63 |
|
|
64 |
my $target_method = $params->{target_method} // $method; |
|
65 |
|
|
66 |
my ($pre_context, $post_context) = ('', ''); |
|
67 |
if (exists $params->{force_context}) { |
|
68 |
$pre_context = $pre_context_by_key { $params->{force_context} }; |
|
69 |
$post_context = $post_context_by_key{ $params->{force_context} }; |
|
70 |
die "invalid context '$params->{force_context}' to force" unless defined $pre_context && defined $post_context; |
|
71 |
} |
|
72 |
|
|
73 |
my $target_code = ucfirst($target) eq $target ? $target : "\$_[0]->$target"; |
|
74 |
|
|
75 |
my $args_string = $args_string_by_key{ $params->{args} // 'standard' }; |
|
76 |
die "invalid args handling '$params->{args}'" unless defined $target_code; |
|
77 |
|
|
78 |
eval " |
|
79 |
sub $caller::$method { |
|
80 |
$pre_context $target_code$method_joiner$target_method$args_string; $post_context |
|
81 |
} |
|
82 |
1; |
|
83 |
" or die "could not create $caller::$method: $@"; |
|
84 |
} |
|
85 |
} |
|
86 |
} |
|
87 |
|
|
88 |
|
|
89 |
|
|
90 |
1; |
|
91 |
|
|
92 |
__END__ |
|
93 |
|
|
94 |
=encoding utf-8 |
|
95 |
|
|
96 |
=head1 NAME |
|
97 |
|
|
98 |
SL::Helper::Object - Meta Object Helper Mixin |
|
99 |
|
|
100 |
=head1 SYNOPSIS |
|
101 |
|
|
102 |
use SL::Helper::Object ( |
|
103 |
delegate => [ |
|
104 |
$target => [ qw(method1 method2 method3 ...) ], |
|
105 |
$target => [ { DELEGATE_OPTIONS }, qw(method1 method2 method3 ...) ], |
|
106 |
... |
|
107 |
], |
|
108 |
); |
|
109 |
|
|
110 |
=head1 DESCRIPTION |
|
111 |
|
|
112 |
Sick of writing getter, setter? No because Rose::Object::MakeMethods got you covered. |
|
113 |
|
|
114 |
Sick of writing all the rest that Rose an't do? Put it here. Functionality in this |
|
115 |
mixin is passed as an include parameter, but are still described as functions: |
|
116 |
|
|
117 |
=head1 FUNCTIONS |
|
118 |
|
|
119 |
=over 4 |
|
120 |
|
|
121 |
=item C<delegate PARAMS> |
|
122 |
|
|
123 |
Creates a method that delegates to the target. If the target string starts with |
|
124 |
a lower case character, the generated code will be called on an object found |
|
125 |
within the calling object by calling an accessor. This way, it is possible to |
|
126 |
delegate to an object: |
|
127 |
|
|
128 |
delegate => [ |
|
129 |
backend_obj => [ qw(save) ], |
|
130 |
], |
|
131 |
|
|
132 |
will generate: |
|
133 |
|
|
134 |
sub save { |
|
135 |
$_[0]->backend_obj->save |
|
136 |
} |
|
137 |
|
|
138 |
If it starts with an upper case letter, it is assumed that it is a class name: |
|
139 |
|
|
140 |
delegate => [ |
|
141 |
'Backend' => [ qw(save) ], |
|
142 |
], |
|
143 |
|
|
144 |
will generate: |
|
145 |
|
|
146 |
sub save { |
|
147 |
Backend->save |
|
148 |
} |
|
149 |
|
|
150 |
Possible delegate args are: |
|
151 |
|
|
152 |
=over 4 |
|
153 |
|
|
154 |
=item * C<target_method> |
|
155 |
|
|
156 |
Optional. If not given, the generated method will dispatch to the same method |
|
157 |
in the target class. If this is not possible, this can be used to overwrite it. |
|
158 |
|
|
159 |
=item * C<args> |
|
160 |
|
|
161 |
Controls how the arguments are passed. |
|
162 |
|
|
163 |
If set to C<none>, the generated code will not bother passing args. This has the benefit |
|
164 |
of not needing to splice the caller class out of @_, or to touch @_ at all for that matter. |
|
165 |
|
|
166 |
If set to C<raw>, the generated code will pass @_ without changes. This will |
|
167 |
result in the calling class or object being left in the arg, but is fine if the |
|
168 |
delegator is called as a function. |
|
169 |
|
|
170 |
If set to C<standard> (which is also the default), the original caller will be |
|
171 |
spliced out and replaced with the new calling context. |
|
172 |
|
|
173 |
=item * C<force_context> |
|
174 |
|
|
175 |
Forces the given context on the delegated method. Valid arguments can be |
|
176 |
C<void>, C<scalar>, C<list>. Default behaviour simply puts the call at the end |
|
177 |
of the sub so that context is propagated. |
|
178 |
|
|
179 |
=item * C<class_function> |
|
180 |
|
|
181 |
If true, the function will be called as a class function instead of a method call. |
|
182 |
|
|
183 |
=back |
|
184 |
|
|
185 |
=back |
|
186 |
|
|
187 |
=head1 BUGS |
|
188 |
|
|
189 |
None yet :) |
|
190 |
|
|
191 |
=head1 AUTHOR |
|
192 |
|
|
193 |
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt> |
|
194 |
|
|
195 |
=cut |
t/helper/object.t | ||
---|---|---|
1 |
use strict; |
|
2 |
use Test::More tests => 37; |
|
3 |
|
|
4 |
use lib 't'; |
|
5 |
|
|
6 |
# to test delegate, test a few of these combinations: |
|
7 |
# target_class or object |
|
8 |
# target_method given or not |
|
9 |
# object or class invocation |
|
10 |
|
|
11 |
{ package T::Helper::Object::Delegatee; |
|
12 |
sub test_simple { "simple" } |
|
13 |
sub test_class { "classic" } |
|
14 |
sub test_invocation { (ref $_[0] ? ref $_[0] : $_[0]) eq __PACKAGE__ } |
|
15 |
sub test_method { !!ref $_[0] } |
|
16 |
sub test_wantarray { |
|
17 |
if (!defined wantarray) { |
|
18 |
${$_[1]} = 'void'; |
|
19 |
} else { |
|
20 |
${$_[1]} = wantarray ? 'list' : 'scalar'; |
|
21 |
} |
|
22 |
} |
|
23 |
sub args { @_ } |
|
24 |
} |
|
25 |
my $delegatee = bless {}, "T::Helper::Object::Delegatee"; |
|
26 |
|
|
27 |
{ |
|
28 |
package T::Helper::Object::Test1; |
|
29 |
use SL::Helper::Object ( |
|
30 |
delegate => [ |
|
31 |
obj => [ "test_simple", "test_invocation", "test_method", "test_wantarray", "args" ], |
|
32 |
obj => [ { target_method => "test_simple" }, "test_simple_renamed" ], |
|
33 |
"T::Helper::Object::Delegatee" => [ "test_class" ], |
|
34 |
"T::Helper::Object::Delegatee" => [ { target_method => "test_class" }, "test_class_renamed" ], |
|
35 |
"T::Helper::Object::Delegatee" => [ { target_method => "test_invocation" }, "test_class_invocation" ], |
|
36 |
"T::Helper::Object::Delegatee" => [ { target_method => "test_method" }, "test_function" ], |
|
37 |
obj => [ { target_method => 'test_wantarray', force_context => 'void' }, 'test_void_context' ], |
|
38 |
obj => [ { target_method => 'test_wantarray', force_context => 'scalar' }, 'test_scalar_context' ], |
|
39 |
obj => [ { target_method => 'test_wantarray', force_context => 'list' }, 'test_list_context' ], |
|
40 |
obj => [ { target_method => 'args', args => 'none' }, 'no_args' ], |
|
41 |
obj => [ { target_method => 'args', args => 'raw' }, 'raw_args' ], |
|
42 |
obj => [ { target_method => 'args', args => 'standard' }, 'standard_args' ], |
|
43 |
"T::Helper::Object::Delegatee" => [ { target_method => "args", args => 'raw' }, "raw_class_args" ], |
|
44 |
"T::Helper::Object::Delegatee" => [ { target_method => "args", args => 'standard' }, "standard_class_args" ], |
|
45 |
"T::Helper::Object::Delegatee" => [ { target_method => "args", args => 'standard', class_function => 1 }, "class_function_args" ], |
|
46 |
], |
|
47 |
); |
|
48 |
sub obj { $_[0]{obj} } |
|
49 |
}; |
|
50 |
my $obj1 = bless { obj => $delegatee }, "T::Helper::Object::Test1"; |
|
51 |
|
|
52 |
is $obj1->test_simple, 'simple', 'simple delegation works'; |
|
53 |
is $obj1->test_simple_renamed, 'simple', 'renamed delegation works'; |
|
54 |
is $obj1->test_class, 'classic', 'class delegation works'; |
|
55 |
is $obj1->test_class_renamed, 'classic', 'renamed class delegation works'; |
|
56 |
ok $obj1->test_invocation, 'object invocation works'; |
|
57 |
ok $obj1->test_class_invocation, 'class invocation works'; |
|
58 |
ok $obj1->test_method, 'method invocation works'; |
|
59 |
ok !$obj1->test_function, 'function invocation works'; |
|
60 |
|
|
61 |
|
|
62 |
# 3: args in [ none, raw,standard ] |
|
63 |
|
|
64 |
is scalar $obj1->no_args("test"), 1, 'args none ignores args'; |
|
65 |
is [$obj1->raw_args("test")]->[0], $delegatee, 'args raw 1'; |
|
66 |
is [$obj1->raw_args("test")]->[1], $obj1, 'args raw 2'; |
|
67 |
is [$obj1->raw_args("test")]->[2], "test", 'args raw 3'; |
|
68 |
is scalar $obj1->raw_args("test"), 3, 'args raw args list'; |
|
69 |
is [$obj1->standard_args("test")]->[0], $delegatee, 'args standard 1'; |
|
70 |
is [$obj1->standard_args("test")]->[1], "test", 'args standard 1'; |
|
71 |
is scalar $obj1->standard_args("test"), 2, 'args standard args list'; |
|
72 |
|
|
73 |
is [$obj1->raw_class_args("test")]->[0], ref $delegatee, 'args raw 1'; |
|
74 |
is [$obj1->raw_class_args("test")]->[1], $obj1, 'args raw 2'; |
|
75 |
is [$obj1->raw_class_args("test")]->[2], "test", 'args raw 3'; |
|
76 |
is scalar $obj1->raw_class_args("test"), 3, 'args raw args list'; |
|
77 |
is [$obj1->standard_class_args("test")]->[0], ref $delegatee, 'args standard 1'; |
|
78 |
is [$obj1->standard_class_args("test")]->[1], "test", 'args standard 1'; |
|
79 |
is scalar $obj1->standard_class_args("test"), 2, 'args standard args list'; |
|
80 |
|
|
81 |
is [$obj1->class_function_args("test")]->[0], 'test', 'args class function standard 1'; |
|
82 |
is scalar $obj1->class_function_args("test"), 1, 'args class function standard args list'; |
|
83 |
|
|
84 |
|
|
85 |
# 4: force_context [ none, void, scalar, list ] |
|
86 |
|
|
87 |
my $c; |
|
88 |
$c = ''; $obj1->test_void_context(\$c); is $c, 'void', 'force context void works'; |
|
89 |
$c = ''; $obj1->test_scalar_context(\$c); is $c, 'scalar', 'force context scalar works'; |
|
90 |
$c = ''; $obj1->test_list_context(\$c); is $c, 'list', 'force context list works'; |
|
91 |
|
|
92 |
# and without forcing: |
|
93 |
$c = ''; $obj1->test_wantarray(\$c); is $c, 'void', 'natural context void works'; |
|
94 |
$c = ''; my $test = $obj1->test_wantarray(\$c); is $c, 'scalar', 'natural context scalar works'; |
|
95 |
$c = ''; my @test = $obj1->test_wantarray(\$c); is $c, 'list', 'natural context list works'; |
|
96 |
|
|
97 |
|
|
98 |
# try stupid stuff that should die |
|
99 |
|
|
100 |
my $dies = 1; |
|
101 |
eval { package T::Helper::Object::Test2; |
|
102 |
SL::Helper::Object->import( |
|
103 |
delegate => [ one => [], "two" ], |
|
104 |
); |
|
105 |
$dies = 0; |
|
106 |
1; |
|
107 |
}; |
|
108 |
ok $dies, 'delegate with uneven number of args dies'; |
|
109 |
|
|
110 |
$dies = 1; |
|
111 |
eval { package T::Helper::Object::Test3; |
|
112 |
SL::Helper::Object->import( |
|
113 |
delegate => {}, |
|
114 |
); |
|
115 |
$dies = 0; |
|
116 |
1; |
|
117 |
}; |
|
118 |
ok $dies, 'delegate with hashref dies'; |
|
119 |
|
|
120 |
$dies = 1; |
|
121 |
eval { package T::Helper::Object::Test4; |
|
122 |
SL::Helper::Object->import( |
|
123 |
delegate => [ |
|
124 |
"List::Util" => [ '{}; print "gotcha"' ], |
|
125 |
], |
|
126 |
); |
|
127 |
$dies = 0; |
|
128 |
1; |
|
129 |
}; |
|
130 |
ok $dies, 'code injection in method names dies'; |
|
131 |
|
|
132 |
$dies = 1; |
|
133 |
eval { package T::Helper::Object::Test5; |
|
134 |
SL::Helper::Object->import( |
|
135 |
delegate => [ |
|
136 |
"print 'this'" => [ 'test' ], |
|
137 |
], |
|
138 |
); |
|
139 |
$dies = 0; |
|
140 |
1; |
|
141 |
}; |
|
142 |
ok $dies, 'code injection in target dies'; |
|
143 |
|
|
144 |
$dies = 1; |
|
145 |
eval { package T::Helper::Object::Test6; |
|
146 |
SL::Helper::Object->import( |
|
147 |
delegate => [ |
|
148 |
"List::Util" => [ { target_method => 'system()' }, 'test' ], |
|
149 |
], |
|
150 |
); |
|
151 |
$dies = 0; |
|
152 |
1; |
|
153 |
}; |
|
154 |
ok $dies, 'code injection in target_method dies'; |
|
155 |
|
|
156 |
$dies = 1; |
|
157 |
eval { package T::Helper::Object::Test6; |
|
158 |
SL::Helper::Object->import( |
|
159 |
delegate => [ |
|
160 |
"List::Util" => [ { target_name => 'test2' }, 'test' ], |
|
161 |
], |
|
162 |
); |
|
163 |
$dies = 0; |
|
164 |
1; |
|
165 |
}; |
|
166 |
ok $dies, 'unkown parameter dies'; |
|
167 |
|
|
168 |
1; |
Auch abrufbar als: Unified diff
Object Helper: delegate