| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Data::Tubes::Plugin::Plumbing; | 
| 2 |  |  |  |  |  |  |  | 
| 3 |  |  |  |  |  |  | # vim: ts=3 sts=3 sw=3 et ai : | 
| 4 |  |  |  |  |  |  |  | 
| 5 | 12 |  |  | 12 |  | 90 | use strict; | 
|  | 12 |  |  |  |  | 28 |  | 
|  | 12 |  |  |  |  | 482 |  | 
| 6 | 12 |  |  | 12 |  | 118 | use warnings; | 
|  | 12 |  |  |  |  | 21 |  | 
|  | 12 |  |  |  |  | 440 |  | 
| 7 | 12 |  |  | 12 |  | 71 | use English qw< -no_match_vars >; | 
|  | 12 |  |  |  |  | 23 |  | 
|  | 12 |  |  |  |  | 105 |  | 
| 8 | 12 |  |  | 12 |  | 5019 | use Data::Dumper; | 
|  | 12 |  |  |  |  | 34 |  | 
|  | 12 |  |  |  |  | 686 |  | 
| 9 | 12 |  |  | 12 |  | 85 | use Scalar::Util qw< blessed >; | 
|  | 12 |  |  |  |  | 24 |  | 
|  | 12 |  |  |  |  | 887 |  | 
| 10 |  |  |  |  |  |  | our $VERSION = '0.738'; | 
| 11 |  |  |  |  |  |  |  | 
| 12 |  |  |  |  |  |  | use Log::Log4perl::Tiny | 
| 13 | 12 |  |  | 12 |  | 84 | qw< :easy :dead_if_first get_logger LOGLEVEL LEVELID_FOR >; | 
|  | 12 |  |  |  |  | 33 |  | 
|  | 12 |  |  |  |  | 127 |  | 
| 14 | 12 |  |  |  |  | 990 | use Data::Tubes::Util qw< | 
| 15 |  |  |  |  |  |  | args_array_with_options | 
| 16 |  |  |  |  |  |  | load_module | 
| 17 |  |  |  |  |  |  | load_sub | 
| 18 |  |  |  |  |  |  | pump | 
| 19 |  |  |  |  |  |  | normalize_args | 
| 20 |  |  |  |  |  |  | traverse | 
| 21 | 12 |  |  | 12 |  | 5077 | >; | 
|  | 12 |  |  |  |  | 24 |  | 
| 22 | 12 |  |  | 12 |  | 4857 | use Data::Tubes::Plugin::Util qw< identify log_helper tubify >; | 
|  | 12 |  |  |  |  | 36 |  | 
|  | 12 |  |  |  |  | 25565 |  | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | sub alternatives { | 
| 25 | 2 |  |  | 2 | 1 | 682 | my ($tubes, $args) = | 
| 26 |  |  |  |  |  |  | args_array_with_options(@_, {name => 'alternatives'}); | 
| 27 | 2 |  |  |  |  | 14 | identify($args); | 
| 28 | 2 |  |  |  |  | 6 | my $name = $args->{name}; | 
| 29 |  |  |  |  |  |  |  | 
| 30 | 2 |  |  |  |  | 9 | my @tubes = tubify($args, @$tubes); | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | return sub { | 
| 33 | 2 |  |  | 2 |  | 5 | my $record = shift; | 
| 34 | 2 |  |  |  |  | 5 | for my $tube (@tubes) { | 
| 35 | 5 | 100 |  |  |  | 25 | if (my @retval = $tube->($record)) { | 
| 36 | 2 |  |  |  |  | 17 | return @retval; | 
| 37 |  |  |  |  |  |  | } | 
| 38 |  |  |  |  |  |  | } | 
| 39 | 0 |  |  |  |  | 0 | return; | 
| 40 | 2 |  |  |  |  | 17 | }; | 
| 41 |  |  |  |  |  |  | } ## end sub alternatives | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | sub _get_selector { | 
| 44 | 14 |  |  | 14 |  | 32 | my $args     = shift; | 
| 45 | 14 |  |  |  |  | 33 | my $selector = $args->{selector}; | 
| 46 | 14 | 100 | 66 |  |  | 91 | if (!defined($selector) && defined($args->{key})) { | 
| 47 | 12 |  |  |  |  | 28 | my $key = $args->{key}; | 
| 48 | 12 |  |  |  |  | 25 | my $ref = ref $key; | 
| 49 |  |  |  |  |  |  | $selector = | 
| 50 |  |  |  |  |  |  | ($ref eq 'CODE') | 
| 51 |  |  |  |  |  |  | ? $key | 
| 52 | 12 | 100 |  | 38 |  | 67 | : sub { return traverse($_[0], $ref ? @$key : $key); }; | 
|  | 38 | 50 |  |  |  | 184 |  | 
| 53 |  |  |  |  |  |  | } ## end if (!defined($selector...)) | 
| 54 |  |  |  |  |  |  | LOGDIE "$args->{name}: required dispatch key or selector" | 
| 55 | 14 | 0 | 33 |  |  | 47 | if (! defined $selector) && (! $args->{missing_ok}); | 
| 56 | 14 |  |  |  |  | 29 | return $selector; | 
| 57 |  |  |  |  |  |  | } ## end sub _get_selector | 
| 58 |  |  |  |  |  |  |  | 
| 59 |  |  |  |  |  |  | sub cache { | 
| 60 | 9 |  |  | 9 | 1 | 10222 | my %args = normalize_args(@_, [{name => 'cache'}, 'tube']); | 
| 61 | 9 |  |  |  |  | 69 | identify(\%args); | 
| 62 | 9 |  |  |  |  | 28 | my $name = $args{name}; | 
| 63 |  |  |  |  |  |  |  | 
| 64 |  |  |  |  |  |  | # the cached tube | 
| 65 | 9 |  |  |  |  | 65 | my ($tube) = tubify(\%args, $args{tube}); | 
| 66 | 9 | 100 |  |  |  | 43 | LOGCROAK "$name: no tube to cache" unless defined $tube; | 
| 67 |  |  |  |  |  |  |  | 
| 68 |  |  |  |  |  |  | # the cache! We will use something compatible with CHI | 
| 69 | 8 |  | 50 |  |  | 34 | my $cache = $args{cache} // {}; | 
| 70 | 8 | 100 |  |  |  | 34 | $cache = ['^Data::Tubes::Util::Cache', repository => $cache] | 
| 71 |  |  |  |  |  |  | if ref($cache) eq 'HASH'; | 
| 72 | 8 | 100 |  |  |  | 49 | if (!blessed($cache)) { | 
| 73 | 7 | 50 |  |  |  | 41 | my ($x, @args) = ref($cache) ? @$cache : $cache; | 
| 74 | 7 | 50 |  |  |  | 47 | $cache = ref($x) ? $x->(@args) : load_module($x)->new(@args); | 
| 75 |  |  |  |  |  |  | } | 
| 76 | 8 | 50 |  |  |  | 153 | my @get_options = $args{get_options} ? @{$args{get_options}} : (); | 
|  | 0 |  |  |  |  | 0 |  | 
| 77 | 8 | 50 |  |  |  | 29 | my @set_options = $args{set_options} ? @{$args{set_options}} : (); | 
|  | 0 |  |  |  |  | 0 |  | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | # what allows me to look in the cache? | 
| 80 | 8 |  |  |  |  | 68 | my $selector = _get_selector({%args, missing_ok => 1}); | 
| 81 |  |  |  |  |  |  | LOGCROAK "missing key or selector, but output is set" | 
| 82 | 8 | 50 | 33 |  |  | 38 | if (! defined $selector) && defined($args{output}); | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | # cleaning trigger, if any | 
| 85 | 8 |  |  |  |  | 18 | my $cleaner = $args{cleaner}; | 
| 86 | 8 | 100 | 66 |  |  | 48 | $cleaner = $cache->can($cleaner) if defined($cleaner) && !ref($cleaner); | 
| 87 |  |  |  |  |  |  |  | 
| 88 |  |  |  |  |  |  | # cloning facility, if needed | 
| 89 | 8 |  |  |  |  | 17 | my $merger = $args{merger}; | 
| 90 | 8 | 50 | 66 |  |  | 29 | $merger = load_sub($merger) if defined($merger) && !ref($merger); | 
| 91 |  |  |  |  |  |  |  | 
| 92 | 8 |  |  |  |  | 21 | my $output = $args{output}; | 
| 93 |  |  |  |  |  |  | return sub { | 
| 94 | 18 |  |  | 18 |  | 30529 | my $record = shift; | 
| 95 | 18 | 50 |  |  |  | 83 | my $key    = $selector ? $selector->($record) : $record; | 
| 96 | 18 |  |  |  |  | 113 | my $data   = $cache->get($key, @get_options); | 
| 97 | 18 | 100 |  |  |  | 149 | if (!$data) {    # MUST be an array reference at this point | 
| 98 | 13 |  |  |  |  | 47 | my @oc = $tube->($record); | 
| 99 | 13 | 100 |  |  |  | 145 | if (scalar(@oc) == 2) { | 
|  |  | 100 |  |  |  |  |  | 
| 100 | 2 | 100 |  |  |  | 15 | my $rcs = ($oc[0] eq 'records') ? $oc[1] : pump($oc[1]); | 
| 101 | 2 | 50 |  |  |  | 9 | $rcs = [map { $_->{$output} } @$rcs] if defined($output); | 
|  | 6 |  |  |  |  | 15 |  | 
| 102 | 2 |  |  |  |  | 7 | $data = [records => $rcs]; | 
| 103 |  |  |  |  |  |  | } | 
| 104 |  |  |  |  |  |  | elsif (scalar @oc) { | 
| 105 | 10 | 50 |  |  |  | 38 | $data = defined($output) ? [$oc[0]{$output}] : \@oc; | 
| 106 |  |  |  |  |  |  | } | 
| 107 |  |  |  |  |  |  | else { | 
| 108 | 1 |  |  |  |  | 3 | $data = \@oc; | 
| 109 |  |  |  |  |  |  | } | 
| 110 |  |  |  |  |  |  |  | 
| 111 | 13 |  |  |  |  | 63 | $cache->set($key, $data, @set_options); | 
| 112 | 13 | 100 |  |  |  | 50 | $cleaner->($cache) if $cleaner; | 
| 113 |  |  |  |  |  |  | } ## end if (!$data) | 
| 114 |  |  |  |  |  |  |  | 
| 115 | 18 | 100 |  |  |  | 50 | return unless scalar @$data; | 
| 116 |  |  |  |  |  |  |  | 
| 117 | 17 | 100 |  |  |  | 52 | if (scalar(@$data) == 1) {    # single record | 
| 118 | 15 | 100 |  |  |  | 37 | return $merger->($record, $output, $data->[0]) if $merger; | 
| 119 | 13 | 50 |  |  |  | 28 | return $data->[0] unless $output; | 
| 120 | 13 |  |  |  |  | 36 | $record->{$output} = $data->[0]; | 
| 121 | 13 |  |  |  |  | 40 | return $record; | 
| 122 |  |  |  |  |  |  | } ## end if (scalar(@$data) == ...) | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | # array of records here | 
| 125 | 2 |  |  |  |  | 6 | my $aref = $data->[1]; | 
| 126 |  |  |  |  |  |  | my $records = | 
| 127 |  |  |  |  |  |  | $merger | 
| 128 | 0 |  |  |  |  | 0 | ? [map { $merger->($record, $output, $_) } @$aref] | 
| 129 |  |  |  |  |  |  | : $output ? [ | 
| 130 |  |  |  |  |  |  | map { | 
| 131 | 2 | 50 |  |  |  | 10 | { %$record, $output => $_ } | 
|  | 6 | 50 |  |  |  | 16 |  | 
| 132 |  |  |  |  |  |  | } @$aref | 
| 133 |  |  |  |  |  |  | ] | 
| 134 |  |  |  |  |  |  | : $aref; | 
| 135 | 2 |  |  |  |  | 8 | return (records => $records); | 
| 136 | 8 |  |  |  |  | 68 | }; | 
| 137 |  |  |  |  |  |  | } ## end sub cache | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | sub dispatch { | 
| 140 | 6 |  |  | 6 | 1 | 1222 | my %args = normalize_args(@_, | 
| 141 |  |  |  |  |  |  | {default => undef, name => 'dispatch', loglevel => $INFO}); | 
| 142 | 6 |  |  |  |  | 36 | identify(\%args); | 
| 143 | 6 |  |  |  |  | 17 | my $name = $args{name}; | 
| 144 |  |  |  |  |  |  |  | 
| 145 | 6 |  |  |  |  | 25 | my $selector = _get_selector(\%args); | 
| 146 |  |  |  |  |  |  |  | 
| 147 | 6 |  |  |  |  | 14 | my $factory = $args{factory}; | 
| 148 | 6 | 50 |  |  |  | 14 | if (!defined($factory)) { | 
| 149 |  |  |  |  |  |  | $factory = sub { | 
| 150 | 0 |  |  | 0 |  | 0 | my ($key, $record) = @_; | 
| 151 | 0 |  |  |  |  | 0 | die { | 
| 152 |  |  |  |  |  |  | message => "$name: unhandled selection key '$key'", | 
| 153 |  |  |  |  |  |  | record  => $record, | 
| 154 |  |  |  |  |  |  | }; | 
| 155 | 0 |  |  |  |  | 0 | }; | 
| 156 |  |  |  |  |  |  | } ## end if (!defined($factory)) | 
| 157 |  |  |  |  |  |  |  | 
| 158 | 6 | 100 |  |  |  | 8 | my %predefined_for = %{$args{handlers} || {}}; | 
|  | 6 |  |  |  |  | 35 |  | 
| 159 | 6 |  |  |  |  | 21 | my %handler_for; | 
| 160 | 6 |  |  |  |  | 13 | my $default = $args{default}; | 
| 161 |  |  |  |  |  |  | return sub { | 
| 162 | 24 |  |  | 24 |  | 247 | my $record = shift; | 
| 163 |  |  |  |  |  |  |  | 
| 164 |  |  |  |  |  |  | # get a key into the cache | 
| 165 | 24 |  | 33 |  |  | 51 | my $key = $selector->($record) // $default; | 
| 166 | 24 | 50 |  |  |  | 52 | die { | 
| 167 |  |  |  |  |  |  | message => "$name: selector key is undefined", | 
| 168 |  |  |  |  |  |  | record  => $record, | 
| 169 |  |  |  |  |  |  | } | 
| 170 |  |  |  |  |  |  | unless defined $key; | 
| 171 |  |  |  |  |  |  |  | 
| 172 |  |  |  |  |  |  | $handler_for{$key} //= exists $predefined_for{$key} | 
| 173 | 24 | 100 | 66 |  |  | 123 | ? (tubify($predefined_for{$key}))[0] | 
| 174 |  |  |  |  |  |  | : (tubify(\%args, $factory->($key, $record)))[0]; | 
| 175 |  |  |  |  |  |  |  | 
| 176 | 24 |  |  |  |  | 76 | return $handler_for{$key}->($record); | 
| 177 | 6 |  |  |  |  | 37 | }; | 
| 178 |  |  |  |  |  |  | } ## end sub dispatch | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | sub fallback { | 
| 181 |  |  |  |  |  |  |  | 
| 182 |  |  |  |  |  |  | # we lose syntax sugar but allow for Try::Tiny to remain optional | 
| 183 | 7 | 50 |  | 7 | 1 | 11832 | eval { require Try::Tiny; } | 
|  | 7 |  |  |  |  | 1178 |  | 
| 184 |  |  |  |  |  |  | or LOGCONFESS 'Data::Tubes::Plugin::Plumbing::fallback ' | 
| 185 |  |  |  |  |  |  | . 'needs Try::Tiny, please install'; | 
| 186 |  |  |  |  |  |  |  | 
| 187 | 7 |  |  |  |  | 4258 | my ($tubes, $args) = args_array_with_options(@_, {name => 'fallback'}); | 
| 188 | 7 |  |  |  |  | 33 | identify($args); | 
| 189 | 7 |  |  |  |  | 18 | my $name = $args->{name}; | 
| 190 |  |  |  |  |  |  |  | 
| 191 | 7 |  |  |  |  | 23 | my @tubes = tubify($args, @$tubes); | 
| 192 | 7 |  |  |  |  | 14 | my $catch = $args->{catch}; | 
| 193 |  |  |  |  |  |  | return sub { | 
| 194 | 9 |  |  | 9 |  | 16 | my $record = shift; | 
| 195 | 9 |  |  |  |  | 17 | for my $tube (@tubes) { | 
| 196 | 23 |  |  |  |  | 37 | my (@retval, $do_fallback); | 
| 197 |  |  |  |  |  |  | Try::Tiny::try( | 
| 198 |  |  |  |  |  |  | sub { | 
| 199 | 23 |  |  |  |  | 2315 | @retval = $tube->($record); | 
| 200 |  |  |  |  |  |  | }, | 
| 201 |  |  |  |  |  |  | Try::Tiny::catch( | 
| 202 |  |  |  |  |  |  | sub { | 
| 203 | 16 | 100 |  |  |  | 295 | $catch->($_, $record) if $catch; | 
| 204 | 15 |  |  |  |  | 89 | $do_fallback = 1; | 
| 205 |  |  |  |  |  |  | } | 
| 206 |  |  |  |  |  |  | ) | 
| 207 | 23 |  |  |  |  | 115 | ); | 
| 208 | 22 | 100 |  |  |  | 224 | return @retval unless $do_fallback; | 
| 209 |  |  |  |  |  |  | } ## end for my $tube (@tubes) | 
| 210 | 1 |  |  |  |  | 50 | return; | 
| 211 | 7 |  |  |  |  | 52 | }; | 
| 212 |  |  |  |  |  |  | } ## end sub fallback | 
| 213 |  |  |  |  |  |  |  | 
| 214 |  |  |  |  |  |  | sub logger { | 
| 215 | 1 |  |  | 1 | 1 | 8 | my %args = normalize_args(@_, {name => 'log pipe', loglevel => $INFO}); | 
| 216 | 1 |  |  |  |  | 5 | identify(\%args); | 
| 217 | 1 |  |  |  |  | 5 | my $loglevel = LEVELID_FOR($args{loglevel}); | 
| 218 | 1 |  |  |  |  | 9 | my $mangler  = $args{target}; | 
| 219 | 1 | 50 |  |  |  | 8 | if (!defined $mangler) { | 
|  |  | 50 |  |  |  |  |  | 
| 220 | 0 |  |  | 0 |  | 0 | $mangler = sub { return shift; } | 
| 221 | 0 |  |  |  |  | 0 | } | 
| 222 |  |  |  |  |  |  | elsif (ref($mangler) ne 'CODE') { | 
| 223 | 0 | 0 |  |  |  | 0 | my @keys = ref($mangler) ? @$mangler : ($mangler); | 
| 224 |  |  |  |  |  |  | $mangler = sub { | 
| 225 | 0 |  |  | 0 |  | 0 | my $record = shift; | 
| 226 | 0 |  |  |  |  | 0 | return traverse($record, @keys); | 
| 227 | 0 |  |  |  |  | 0 | }; | 
| 228 |  |  |  |  |  |  | } ## end elsif (ref($mangler) ne 'CODE') | 
| 229 | 1 |  |  |  |  | 4 | my $logger = get_logger(); | 
| 230 |  |  |  |  |  |  | return sub { | 
| 231 | 3 |  |  | 3 |  | 7 | my $record = shift; | 
| 232 | 3 |  |  |  |  | 10 | $logger->log($loglevel, $mangler->($record)); | 
| 233 | 3 |  |  |  |  | 16 | return $record; | 
| 234 | 1 |  |  |  |  | 11 | }; | 
| 235 |  |  |  |  |  |  | } ## end sub logger | 
| 236 |  |  |  |  |  |  |  | 
| 237 |  |  |  |  |  |  | sub pipeline { | 
| 238 | 0 |  |  | 0 | 1 | 0 | my ($tubes, $args) = args_array_with_options(@_, {name => 'pipeline'}); | 
| 239 | 0 |  |  |  |  | 0 | return sequence(%$args, tubes => $tubes); | 
| 240 |  |  |  |  |  |  | } | 
| 241 |  |  |  |  |  |  |  | 
| 242 |  |  |  |  |  |  | sub sequence { | 
| 243 | 15 |  |  | 15 | 1 | 1468 | my %args = | 
| 244 |  |  |  |  |  |  | normalize_args(@_, [{name => 'sequence', tubes => []}, 'tubes']); | 
| 245 | 15 |  |  |  |  | 83 | identify(\%args); | 
| 246 |  |  |  |  |  |  |  | 
| 247 |  |  |  |  |  |  | # cope with an empty list of tubes - equivalent to an "id" function but | 
| 248 |  |  |  |  |  |  | # always returning an iterator for consistency | 
| 249 | 15 |  | 50 |  |  | 71 | my $tubes = $args{tubes} || []; | 
| 250 |  |  |  |  |  |  | return sub { | 
| 251 | 0 |  |  | 0 |  | 0 | my @record = shift; | 
| 252 |  |  |  |  |  |  | return ( | 
| 253 |  |  |  |  |  |  | iterator => sub { | 
| 254 | 0 | 0 |  |  |  | 0 | return unless @record; | 
| 255 | 0 |  |  |  |  | 0 | return shift @record; | 
| 256 |  |  |  |  |  |  | } | 
| 257 | 0 |  |  |  |  | 0 | ); | 
| 258 |  |  |  |  |  |  | } | 
| 259 | 15 | 50 |  |  |  | 42 | unless @$tubes; | 
| 260 |  |  |  |  |  |  |  | 
| 261 |  |  |  |  |  |  | # auto-generate tubes if you get definitions | 
| 262 | 15 |  |  |  |  | 65 | my @tubes = tubify(\%args, @$tubes); | 
| 263 |  |  |  |  |  |  |  | 
| 264 | 15 |  | 100 |  |  | 71 | my $gate = $args{gate} // undef; | 
| 265 |  |  |  |  |  |  |  | 
| 266 | 15 |  |  |  |  | 50 | my $logger = log_helper(\%args); | 
| 267 | 15 |  |  |  |  | 34 | my $name   = $args{name}; | 
| 268 |  |  |  |  |  |  | return sub { | 
| 269 | 16 |  |  | 16 |  | 968 | my $record = shift; | 
| 270 | 16 | 50 |  |  |  | 65 | $logger->($record, \%args) if $logger; | 
| 271 |  |  |  |  |  |  |  | 
| 272 | 16 |  |  |  |  | 50 | my @stack = ({record => $record}); | 
| 273 |  |  |  |  |  |  | my $iterator = sub { | 
| 274 |  |  |  |  |  |  | STEP: | 
| 275 | 60 |  |  |  |  | 51109 | while (@stack) { | 
| 276 | 184 |  |  |  |  | 306 | my $pos = $#stack; | 
| 277 |  |  |  |  |  |  |  | 
| 278 | 184 |  |  |  |  | 284 | my $f = $stack[$pos]; | 
| 279 |  |  |  |  |  |  | my @record = | 
| 280 |  |  |  |  |  |  | exists($f->{record})   ? delete $f->{record} | 
| 281 |  |  |  |  |  |  | : exists($f->{iterator}) ? $f->{iterator}->() | 
| 282 | 184 | 100 |  |  |  | 568 | : @{$f->{records} || []} ? shift @{$f->{records}} | 
|  | 85 | 100 |  |  |  | 327 |  | 
|  | 18 | 100 |  |  |  | 39 |  | 
|  |  | 100 |  |  |  |  |  | 
| 283 |  |  |  |  |  |  | :                          (); | 
| 284 | 184 | 100 |  |  |  | 403 | if (!@record) {    # no more at this level... | 
| 285 | 78 |  |  |  |  | 108 | my $n = @stack; | 
| 286 | 78 |  |  |  |  | 306 | TRACE "$name: level $n backtracking, no more records"; | 
| 287 | 78 |  |  |  |  | 894 | pop @stack; | 
| 288 | 78 |  |  |  |  | 279 | next STEP; | 
| 289 |  |  |  |  |  |  | } ## end if (!@record) | 
| 290 |  |  |  |  |  |  |  | 
| 291 | 106 |  |  |  |  | 165 | my $record = $record[0]; | 
| 292 | 106 | 100 |  |  |  | 296 | return $record if @stack > @tubes;    # output cache | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | # cut the sequence early if the gate function says so | 
| 295 | 69 | 100 | 100 |  |  | 177 | return $record if $gate && ! $gate->($record); | 
| 296 |  |  |  |  |  |  |  | 
| 297 |  |  |  |  |  |  | # something must be done... | 
| 298 | 62 | 50 |  |  |  | 221 | my @outcome = $tubes[$pos]->($record) | 
| 299 |  |  |  |  |  |  | or next STEP; | 
| 300 |  |  |  |  |  |  |  | 
| 301 | 62 | 100 |  |  |  | 399 | unshift @outcome, 'record' if @outcome == 1; | 
| 302 | 62 |  |  |  |  | 227 | push @stack, {@outcome};              # and go to next level | 
| 303 |  |  |  |  |  |  | } ## end STEP: while (@stack) | 
| 304 |  |  |  |  |  |  |  | 
| 305 | 16 |  |  |  |  | 49 | return;    # end of output, empty list | 
| 306 | 16 |  |  |  |  | 77 | }; | 
| 307 | 16 |  |  |  |  | 63 | return (iterator => $iterator); | 
| 308 | 15 |  |  |  |  | 106 | }; | 
| 309 |  |  |  |  |  |  | } ## end sub sequence | 
| 310 |  |  |  |  |  |  |  | 
| 311 |  |  |  |  |  |  | 1; |