Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 963330cc

Von Sven Schöling vor mehr als 7 Jahren hinzugefügt

  • ID 963330ccde9da7cb7bb390c1c52dbe2e051d67da
  • Vorgänger 41dae9cd
  • Nachfolger bae8f672

Object Helper: delegate

Unterschiede anzeigen:

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