File Coverage

blib/lib/Extender.pm
Criterion Covered Total %
statement 155 181 85.6
branch 36 70 51.4
condition 15 47 31.9
subroutine 30 34 88.2
pod 8 8 100.0
total 244 340 71.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             ################################################################################
3             #
4             # Extender - Reference-Scalar-Object method Extender.
5             #
6             # (C) 2024 OnEhIppY - Domero Software
7             #
8             ################################################################################
9             package Extender;
10              
11 1     1   99663 use strict;
  1         1  
  1         30  
12 1     1   3 use warnings;
  1         5  
  1         51  
13 1     1   4 use Exporter 'import';
  1         1  
  1         113  
14              
15             our $VERSION = '1.01';
16             our @EXPORT = qw(Extend Extends GlobExtends Alias AddMethod Decorate ApplyRole InitHook Unload);
17              
18             ################################################################################
19              
20             sub Extend {
21 3     3 1 197351 my ($object, $module, @methods) = @_;
22              
23             # Check if the module is already loaded
24 3 50 33     16 unless (exists $INC{$module} || defined *{"${module}::"}) {
  3         22  
25 0         0 eval "require $module";
26 0 0       0 return undef if $@;
27             }
28              
29             # Get list of functions exported by the module
30 1     1   5 no strict 'refs';
  1         2  
  1         188  
31              
32             # Add each specified function (or all if none specified) as a method to the object
33 3 50       12 foreach my $func ($#methods > -1 ? @methods : @{"${module}::EXPORT"}) {
  0         0  
34 5     0   24 *{ref($object) . "::$func"} = sub { unshift @_, $object; goto &{"${module}::$func"} };
  5         32  
  0         0  
  0         0  
  0         0  
35             }
36              
37 3         9 return $object;
38             }
39              
40             ################################################################################
41              
42             sub Extends {
43 3     3 1 2912 my ($object, %extend) = @_;
44              
45 3         13 for my $name (keys %extend) {
46             # Create the method
47 1     1   4 no strict 'refs';
  1         2  
  1         463  
48 4   33     15 my $package = ref($object) || $object; # Get the package or class name
49              
50 4 50 0     35 if (ref $extend{$name} eq 'CODE') {
    0 0        
51             # If $extend{$name} is a coderef, directly assign it
52 4         37 *{$package . "::$name"} = sub {
53 0     0   0 my $self = shift;
54 0         0 return $extend{$name}->($self, @_);
55 4         19 };
56             }
57 0         0 elsif (ref $extend{$name} eq 'SCALAR' && defined ${$extend{$name}} && ref ${$extend{$name}} eq 'CODE') {
  0         0  
58             # If $method_ref is a reference to a scalar containing a coderef
59 0         0 *{$package . "::$name"} = sub {
60 0     0   0 my $self = shift;
61 0         0 return ${$extend{$name}}->($self, @_);
  0         0  
62 0         0 };
63             }
64             else {
65 0         0 die "Invalid method reference provided for $name. Expected CODE or reference to CODEREF but got ".(ref($extend{$name})).".";
66             }
67             }
68              
69 3         8 return $object;
70             }
71              
72             ################################################################################
73              
74             sub Alias {
75 1     1 1 1239 my ($object, $existing_method, $new_name) = @_;
76              
77             # Check if $object is a blessed reference
78 1 50 33     15 die "Not a valid object reference" unless ref $object && ref $object ne 'HASH' && ref $object ne 'ARRAY' && ref $object ne 'SCALAR';
      33        
      33        
79              
80             # Validate $existing_method
81 1 50 33     12 die "Invalid method name. Method name must be a string" unless defined $existing_method && $existing_method =~ /^\w+$/;
82              
83             # Validate $new_name
84 1 50 33     28 die "Invalid alias name. Alias name must be a string" unless defined $new_name && $new_name =~ /^\w+$/;
85              
86             # Create the alias within the package where $object is blessed into
87             {
88 1     1   7 no strict 'refs';
  1         1  
  1         24  
  1         3  
89 1     1   3 no warnings 'redefine';
  1         2  
  1         184  
90 1         4 my $pkg = ref($object);
91 1         2 *{$pkg . "::$new_name"} = \&{$pkg . "::$existing_method"};
  1         8  
  1         6  
92             }
93              
94 1         3 return $object;
95             }
96              
97             ################################################################################
98              
99             sub AddMethod {
100 1     1 1 1071 my ($object, $method_name, $code_ref) = @_;
101              
102             # Validate method name
103 1 50 33     13 die "Method name must be a string" unless defined $method_name && $method_name =~ /^\w+$/;
104              
105             # Validate code reference
106 1 50       14 die "Code reference required" unless ref($code_ref) eq 'CODE';
107              
108 1     1   8 no strict 'refs';
  1         1  
  1         76  
109 1         3 *{ref($object) . "::$method_name"} = $code_ref;
  1         9  
110              
111 1         4 return $object;
112             }
113              
114             ################################################################################
115              
116             sub Decorate {
117 1     1 1 1424 my ($object, $method_name, $decorator) = @_;
118              
119             # Check if $object is an object or a class name
120 1 50       6 my $is_object = ref($object) ? 1 : 0;
121              
122             # Fetch the original method reference
123 1         3 my $original_method;
124 1 50       3 if ($is_object) {
125 1     1   4 no strict 'refs';
  1         1  
  1         40  
126 1         8 my $coderef = $object->can($method_name);
127 1 50       5 die "Method $method_name does not exist in the object" unless $coderef;
128 1         2 $original_method = $coderef;
129             } else {
130 1     1   3 no strict 'refs';
  1         1  
  1         40  
131 0         0 $original_method = *{$object . '::' . $method_name}{CODE};
  0         0  
132 0 0       0 die "Method $method_name does not exist in the package" unless defined $original_method;
133             }
134              
135             # Replace the method with a decorated version
136 1 50       4 if ($is_object) {
137 1     1   3 no strict 'refs';
  1         1  
  1         20  
138 1         3 my $class = ref $object;
139 1     1   3 no warnings 'redefine';
  1         1  
  1         69  
140 1         8 *{$class . "::$method_name"} = sub {
141 1     1   5 my $self = shift;
142 1         4 return $decorator->($self, $original_method, @_);
143 1         6 };
144             } else {
145 1     1   4 no strict 'refs';
  1         1  
  1         43  
146 1     1   3 no warnings 'redefine';
  1         7  
  1         308  
147 0         0 *{$object . "::$method_name"} = sub {
148 0     0   0 my $self = shift;
149 0         0 return $decorator->($self, $original_method, @_);
150 0         0 };
151             }
152              
153 1         3 return $object
154             }
155              
156             ################################################################################
157              
158             sub ApplyRole {
159              
160 3     3 1 2329 my ($object, $role_class) = @_;
161              
162 3 50       12 die "Object must be provided for role application" unless defined $object;
163 3 50 33     33 die "Role class must be specified" unless defined $role_class && $role_class =~ /^\w+$/;
164              
165             # Ensure role class is loaded
166 3 100 66     32 unless (exists $INC{$role_class} || defined *{"${role_class}::"}) {
  3         19  
167 1         194 eval "require $role_class";
168 1 50       35 return undef if $@;
169             }
170              
171             # Apply the role's methods to the object if the apply method exists
172 2         5 eval {
173 1     1   5 no strict 'refs';
  1         5  
  1         303  
174 2         27 my $apply_method = $role_class->can('apply');
175 2 100       7 if ($apply_method) {
176 1         4 $apply_method->($role_class, $object);
177             } else {
178 1         13 die "Role $role_class does not implement apply method";
179             }
180             };
181 2 100       28 if ($@) {
182 1 50       24 if ($@ =~ /Role $role_class does not implement apply method/) {
183 1         5 return undef; # Return gracefully if the apply method is missing
184             } else {
185 0         0 die "Failed to apply role $role_class to object: $@";
186             }
187             }
188              
189 1         3 return $object
190             }
191              
192             ################################################################################
193              
194             sub InitHook {
195 2     2 1 1394 my ($class, $hook_name, $hook_code) = @_;
196              
197             # Validate arguments
198 2 50 33     24 die "Class name must be specified" unless defined $class && $class =~ /^\w+$/;
199 2 50       12 die "Unsupported hook name '$hook_name'" unless $hook_name =~ /^(INIT|DESTRUCT)$/;
200              
201 1     1   4 no strict 'refs';
  1         2  
  1         60  
202            
203             # Initialize hooks array if not already present
204 2   50     41 $class->{"_${hook_name}_hooks"} ||= [];
205            
206             # Register the hook code
207 2         4 push @{$class->{"_${hook_name}_hooks"}}, $hook_code;
  2         8  
208            
209             # If INIT hook, wrap the new method to execute hooks
210 2 100       9 if ($hook_name eq 'INIT') {
    50          
211 1         12 my $original_new = $class->can('new');
212 1     1   3 no warnings 'redefine';
  1         1  
  1         99  
213 1         6 *{$class . "::new"} = sub {
214 2     2   36 my $self = $original_new->(@_);
215 2 50       11 for my $hook (@{$class->{"_INIT_hooks"} || []}) {
  2         12  
216 2         5 $hook->($self);
217             }
218 2         13 return $self;
219 1         6 };
220             }
221            
222             # If DESTRUCT hook, wrap the DESTROY method to execute hooks
223             elsif ($hook_name eq 'DESTRUCT') {
224 1         9 my $original_destroy = $class->can('DESTROY');
225 1     1   5 no warnings 'redefine';
  1         2  
  1         273  
226 1         5 *{$class . "::DESTROY"} = sub {
227 1     1   6 my $self = shift;
228 1 50       2 for my $hook (@{$class->{"_DESTRUCT_hooks"} || []}) {
  1         4  
229 1         3 $hook->($self);
230             }
231 1 50 33     10 $original_destroy->($self) if $original_destroy && ref($self);
232 1         7 };
233             }
234              
235 2         6 return $class;
236             }
237              
238             ################################################################################
239              
240             sub Unload {
241 1     1 1 1587 my ($object, @methods) = @_;
242              
243             # Check if $object is a valid reference and not a CODE reference
244 1         3 my $ref_type = ref $object;
245 1 50 33     8 die "Not a valid object reference" unless $ref_type && $ref_type ne 'CODE';
246              
247             # Validate @methods
248 1 50       4 die "No methods specified for unloading" unless @methods;
249              
250             # Determine the package or type of the reference
251 1         4 my $pkg = ref $object;
252 1 50       4 if ($ref_type eq 'GLOB') {
253             # Use the GLOB reference directly as the package
254 0         0 $pkg = *{$object}{PACKAGE};
  0         0  
255             }
256 1 50       4 die "Cannot determine package for object reference" unless $pkg;
257              
258 1     1   5 no strict 'refs';
  1         2  
  1         129  
259              
260 1         3 foreach my $method (@methods) {
261 1 50       3 next unless defined $method; # Skip if method is undefined
262              
263             # Check if the method exists in the package's symbol table
264 1 50       3 if (exists ${$pkg."::"}{$method}) {
  1         5  
265             # Remove the method from the package's symbol table
266 1         3 delete ${$pkg."::"}{$method};
  1         11  
267             }
268             }
269              
270 1         4 return $object;
271             }
272              
273             ################################################################################
274              
275             1; # EOF Extender.pm (C) 2024 OnEhIppY - Domero Sofware