File Coverage

blib/lib/Sub/Middler.pm
Criterion Covered Total %
statement 71 81 87.6
branch 21 34 61.7
condition n/a
subroutine 13 13 100.0
pod 3 4 75.0
total 108 132 81.8


line stmt bran cond sub pod time code
1             package Sub::Middler;
2 2     2   237426 use 5.024000;
  2         9  
3 2     2   17 use strict;
  2         11  
  2         59  
4 2     2   18 use warnings;
  2         5  
  2         154  
5 2     2   13 use feature "refaliasing";
  2         6  
  2         468  
6              
7              
8             our $VERSION = 'v0.4.0';
9 2     2   1257 use Export::These qw;
  2         2032  
  2         15  
10              
11             sub new {
12             #simply an array...
13 2     2 1 245007 bless [], __PACKAGE__;
14             }
15              
16             # register sub refs to middleware makers
17             sub register {
18 2     2   2413 no warnings "experimental";
  2         5  
  2         340  
19 7     7 1 1278 \my @middleware=$_[0]; #self
20 7         10 my $sub=$_[1];
21             #die "Middleware must be a CODE reference" unless ref($sub) eq "CODE";
22 7         12 push @middleware, $sub;
23 7         13 return $_[0]; #allow chaining
24             }
25              
26             *append=\®ister;
27             *add=\®ister;
28              
29              
30             # Link together sub and give each one an index
31             # Required argument is the 'dispatcher' which is the end point to call
32             #
33             sub _sink_sub;
34             sub link {
35 2     2   14 no warnings "experimental";
  2         4  
  2         3680  
36              
37             #die "A CODE reference is required when linking middleware" unless(@_ >=2 and ref $_[1] eq "CODE");
38            
39              
40 2     2 1 13 \my @self=shift; #self;
41              
42 2         18 my $dispatcher=_sink_sub shift, 1;
43              
44              
45 2         14 my @args=@_;
46              
47 2         5 my @mw; # The generated subs
48              
49 2         6 my @middleware=@self;
50 2         8 for(@middleware){
51 7         30 $_=_sink_sub $_;
52             }
53              
54 2         9 for my $i (reverse 0..@middleware-1){
55 7         36 my $maker=$middleware[$i];
56 7 100       18 my $next=($i==@middleware-1)?$dispatcher:$mw[$i+1];
57            
58              
59 7         18 $mw[$i]=$maker->($next, $i, @args);
60             }
61              
62 2 50       21 @middleware?$mw[0]:$dispatcher;
63             }
64              
65             sub linker {
66 1     1 0 220223 my $dispatch=pop;
67              
68 1         10 my $chain=Sub::Middler->new;
69 1         5 $chain->register($_) for @_;
70 1         5 $chain->link($dispatch);
71            
72             }
73              
74             sub _sink_sub {
75 9     9   16 my $in=$_[0];
76 9         15 my $is_dispatch=$_[1];
77              
78 9 100       30 return $in if ref $in eq "CODE";
79              
80             my $wrap=sub {
81 5     5   5 my $next=shift;
82 5         4 my $out;
83              
84 5         13 for (ref $in){
85              
86              
87 5 100       19 if(/SCALAR/){
    100          
    100          
    50          
88             $out=$is_dispatch
89             ?sub {
90 0         0 $$in.="@{$_[0]}";
  0         0  
91 0 0       0 $_[1] and $_[1]->(); # Auto call call back
92             }
93             :sub {
94             #Convert into string
95 3         5 $$in.="@{$_[0]}";
  3         11  
96 3         6 &$next;
97             }
98 1 50       4 }
99              
100             elsif(/ARRAY/){
101              
102             $out=$is_dispatch
103             ?sub {
104             # Copy and append into array,
105 3         5 push @$in, @{$_[0]};
  3         9  
106 3 50       28 $_[1] and $_[1]->();
107             }
108             :sub {
109             # Copy and append into array,
110 3         111 push @$in, @{$_[0]};
  3         9  
111 3         7 &$next;
112             }
113 2 100       30 }
114              
115             elsif(/HASH/) {
116             $out=$is_dispatch
117             ?sub {
118             # copy into hash
119 0         0 for (my $i=0; $i<$_[0]->@*; $i+=2){
120 0         0 $in->{$_[0][$i]}=$_[0][$i+1];
121             }
122             ############################
123             # for my($k,$v)(@{$_[0]}){ #
124             # $in->{$k}=$v; #
125             # } #
126             ############################
127 0 0       0 $_[1] and $_[1]->();
128             }
129             :sub {
130             # copy into hash
131 3         8 for (my $i=0; $i<$_[0]->@*; $i+=2){
132 9         27 $in->{$_[0][$i]}=$_[0][$i+1];
133             }
134             ############################
135             # for my($k,$v)(@{$_[0]}){ #
136             # $in->{$k}=$v; #
137             # } #
138             ############################
139 3         7 &$next;
140             }
141 1 50       17 }
142              
143              
144             elsif(/REF/){
145 1         2 my $r=$$in;
146 1 50       4 if(ref $r eq "CODE"){
147             # treat a ref to a code ref as
148             $out=$is_dispatch
149             ?sub {
150 0         0 my @res=&$r;
151 0 0       0 $_[1] and $_[1]->();
152             }
153              
154             :sub {
155 3         7 my @res=&$r;
156             #$next->(@res);
157 3         22 &$next;
158             }
159 1 50       5 }
160             else {
161 0         0 die "should not get here";
162             }
163             }
164             else {
165 0         0 die "Could not link unkown reference: ". ref $in;
166             }
167             }
168 5         9 $out;
169 5         17 };
170 5 100       12 $is_dispatch?$wrap->():$wrap;
171             }
172              
173             1;
174              
175             =head1 NAME
176              
177             Sub::Middler - Middleware subroutine chaining
178              
179             =head1 SYNOPSIS
180              
181             use strict;
182             use warings;
183             use Sub::Middler;
184              
185            
186             my @array;
187             my %hash;
188             my $scalar;
189              
190             # append results in variables
191             my $head=linker
192             # Short cut to store (copy/append) in array
193             \@array
194             # Short cut to modifiy inputs
195             =>\sub { $_*=2 for @{$_[0]}},
196             # Short cut to store in hash
197             =>\%hash,
198             # Short cut to stringyfiy and append to scalar
199             =>\$scalar;
200            
201              
202             $head->([1,2,3,4,], sub {...})
203             # inputs ready cb
204              
205              
206             use strict;
207             use warnings;
208             use Sub::Middler;
209              
210             my $middler=Sub::Middler->new;
211              
212             $middler->register(mw1(x=>1));
213             $middler->register(mw2(y=>10));
214              
215             my $head=$middler->link(
216             sub {
217             print "Result: $_[0]\n";
218             }
219             );
220              
221             $head->(0); # Call the Chain
222              
223             # Middleware 1
224             sub mw1 {
225             my %options=@_;
226             sub {
227             my ($next, $index, @optional)=@_;
228             sub {
229             my $work=$_[0]+$options{x};
230             $next->($work);
231             }
232             }
233             }
234              
235             # Middleware 2
236             sub mw2 {
237             my %options=@_;
238             sub {
239             my ($next, $index, @optional)=@_;
240             sub {
241             my $work= $_[0]*$options{y};
242             $next->( $work);
243             }
244             }
245             }
246              
247             =head1 DESCRIPTION
248              
249             A small module, facilitating linking subroutines together, acting as middleware
250             ,filters or chains with low runtime overhead.
251              
252             To achieve this, the 'complexity' is offloaded to the definition of
253             middleware/filters subroutines. They must be wrapped in subroutines
254             appropriately to facilitate the lexical binding of linking variables.
255              
256             This differs from other 'sub chaining' modules as it does not use a loop
257             internally to iterate over a list of subroutines at runtime. As such there is
258             no implicit synchronous call to the 'next' item in the chain. Each stage can run
259             the following stage synchronously or asynchronously or not at all. Each element
260             in the chain is responsible for how and when it calls the 'next'.
261              
262             Finally the arguments and signatures of each stage of middleware are completely
263             user defined and are not interfered with by this module. This allows reuse of
264             the C<@_> array in calling subsequent stages for ultimate performance if you
265             know what you're doing.
266              
267             As a general guide it's suggested the last argument to a stage be a subroutine
268             reference to allow callbacks and asynchronous usage. Instead of a flat list of
269             multiple inputs into a stage, it is suggested to also contain these in an array
270              
271             From v0.4.0, shortcuts can be used to to bypass writing the nestled
272             subroutines subroutines for some common use cases. A reference to a
273             SCALAR/ARRAY/HASH/CODE can be used instead of custom middleware
274              
275             =head1 API
276              
277             =head2 Inline linking
278              
279             linker mw1, ..., dispatch
280              
281             From v0.3.0, the C subroutine is exported and will do an inline build
282             and link for a given middleware and dispatch routine
283              
284             The return value is the head of the linked chain, and is equivalent to created
285             a C object, adding middleware, and the calling the link method.
286              
287              
288             =head2 Short Cuts
289              
290            
291             Instead of writing custom middleware, references to variables and CODE can be
292             used instead.
293              
294             If an array reference is used, all elements from the first argument will be
295             appended to the array
296              
297             If an hash reference is used, the elements from the first argument will be
298             treated as key value pairs and set the corresponding elements in the target
299             hash
300              
301             If a scalar reference is use, the elements from the first argument will be
302             converted to strings and appending to the target variable
303              
304              
305             If a reference is a CODE reference is used, the underlying subroutine is
306             expected to modify the first argument elements in place. The return value is
307             not used.
308              
309              
310             In all the above cases, the next link in the chain is automatically called with
311             the same arguments, making chaining and saving intermediate values easy
312              
313              
314             =head2 Managing a chain
315              
316             =head3 new
317            
318             my $object=Sub::Middler->new;
319              
320             Creates a empty middler object ready to accept middleware. The object is a
321             blessed array reference which stores the middleware directly.
322              
323             =head3 register
324              
325             $object->register(my_middlware());
326              
327             Appends the middleware to the internal list for later linking.
328              
329             =head3 append, add
330              
331             Alias for register
332              
333             =head3 link
334              
335             $object->link($last,[@args]);
336              
337             Links together the registered middleware in the sequence of addition. Each
338             middleware is intrinsically linked to the next middleware in the list. The last
339             middleware being linked to the C<$last> argument, which must be a code ref.
340              
341             The C<$last> ref MUST be a regular subroutine reference, acting as the
342             'kernel' as described in following sections.
343              
344             Calls C if C<$last> is not a code ref.
345              
346             Any optional additional arguments C<@args> are passed to this function are
347             passed on to each 'maker' sub after the C<$next> and C<$index>, parameters.
348             This gives an alternative approach to distributing configuration data to each
349             item in the chain prior to runtime. It is up to each item's maker sub to store
350             relevant passed values as they see fit.
351              
352             =head2 Creating Middleware
353              
354             To achieve low over head in linking middleware, functional programming
355             techniques (higher order functions) are utilised. This also give the greatest
356             flexibility to the middleware, as signatures are completely user defined.
357              
358             The trade off is that the middleware must be defined in a certain code
359             structure. While this isn't difficult, it takes a minute to wrap your head
360             around.
361              
362              
363             =head3 Middlware Definition
364              
365             Middleware must be a subroutine (top/name) which returns a anonymous subroutine
366             (maker), which also returns a anonymous subroutine to perform work (kernel).
367              
368             This sounds complicated by this is what is looks like in code:
369              
370             sub my_middleware { (1) Top/name subroutine
371             my %options=@_; Store any config
372            
373             sub { (2) maker sub is returned
374             my ($next, $index, @optional)=@_; (3) Must store at least $next
375              
376             sub { (4) Returns the kernel sub
377             # Code here implements your middleware
378             # %options are lexically accessable here
379             # as are the @optional parameters
380            
381              
382             # Execute the next item in the chain
383             $next->(...); (5) Does work and calls the next entry
384              
385              
386             (6) Post work if applicable
387             }
388             }
389             }
390              
391             =over
392              
393             =item Top Subroutine
394              
395             The top sub routine (1) can take any arguments you desire and can be called
396             what you like. The idea is it represents your middleware/filter and stores any
397             setup lexically for the B sub to close over. It returns the B
398             sub.
399              
400             =item Maker Subroutine
401              
402             This anonymous sub (2) closes over the variables stored in B and is the
403             input to this module (via C). When being linked (called) by this
404             module it is provided at least two arguments: the reference to the next item in
405             the chain and the current middleware index. These B be stored to be
406             useful, but can be called anything you like (3).
407            
408             Any optional/additional arguments supplied during a call to C are also
409             used as arguments 'as is' to all maker subroutines in the chain.
410              
411              
412             =item Kernel subroutine
413              
414             This anonymous subroutine (4) actually performs the work of the
415             middleware/filter. After work is done, the next item in the chain must be
416             called explicitly (5). This supports synchronous or asynchronous middleware.
417             Any extra work can be performed after the chain is completed after this call
418             (6).
419              
420             =back
421              
422              
423             =head2 LINKING CHAINS
424              
425             Multiple chains of middleware can be linked together. This needs to be done in
426             reverse order. The last chain after being linked, becomes the C<$last> item
427             when linking the preceding chain and so on.
428              
429              
430             =head2 EXAMPLES
431              
432             The synopsis example can be found in the examples directory of this
433             distribution.
434              
435              
436             =head1 SEE ALSO
437              
438             L and L links together subs. They provide other
439             features that this module does not.
440              
441             These iterate over a list of subroutines at runtime to achieve named subs etc.
442             where as this module pre links subroutines together, reducing overhead.
443              
444              
445             =head1 AUTHOR
446              
447             Ruben Westerberg, Edrclaw@mac.comE
448              
449             =head1 REPOSITORTY and BUGS
450              
451             Please report any bugs via git hub: L
452              
453             =head1 COPYRIGHT AND LICENSE
454              
455             Copyright (C) 2025 by Ruben Westerberg
456              
457             This library is free software; you can redistribute it
458             and/or modify it under the same terms as Perl or the MIT
459             license.
460              
461             =head1 DISCLAIMER OF WARRANTIES
462              
463             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS
464             OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE
465             IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
466             PARTICULAR PURPOSE.
467             =cut
468