File Coverage

blib/lib/Data/Resolver.pm
Criterion Covered Total %
statement 179 197 90.8
branch 61 78 78.2
condition 19 38 50.0
subroutine 27 30 90.0
pod 16 16 100.0
total 302 359 84.1


line stmt bran cond sub pod time code
1             package Data::Resolver;
2 5     5   1023692 use v5.24;
  5         20  
3 5     5   32 use Carp;
  5         11  
  5         512  
4 5     5   3448 use English qw< -no_match_vars >;
  5         15431  
  5         35  
5 5     5   3191 use experimental qw< signatures >;
  5         2046  
  5         39  
6             { our $VERSION = '0.006' }
7              
8 5     5   12320 use JSON::PP qw< decode_json >;
  5         91680  
  5         528  
9              
10 5     5   49 use Exporter qw< import >;
  5         10  
  5         18485  
11             my @FACTORIES = qw<
12             generate
13             resolver_from_dir
14             resolver_from_passthrough
15             resolver_from_tar
16             >;
17             my @INTEGRATION = qw<
18             resolved
19             resolved_error
20             resolved_error_factory
21             resolved_factory
22             >;
23             my @TRANSFORMERS = qw<
24             data_to_fh
25             data_to_file
26             fh_to_data
27             fh_to_file
28             file_to_data
29             file_to_fh
30             transform
31             >;
32             our @EXPORT_OK = (@FACTORIES, @INTEGRATION, @TRANSFORMERS);
33             our %EXPORT_TAGS = (
34             all => [@EXPORT_OK],
35             factories => [@FACTORIES],
36             integration => [@INTEGRATION],
37             transformers => [@TRANSFORMERS],
38             );
39              
40             # ----------------------------------------------------------------------
41             # Factories
42 12     12 1 232491 sub generate ($spec) {
  12         19  
  12         22  
43 12 50       41 $spec = decode_json($spec) unless ref($spec);
44 12         66 my %args = $spec->%*;
45              
46 12   50     70 my $package = delete($args{'-package'}) // __PACKAGE__;
47 12         85 my $path = "$package.pm" =~ s{::}{/}rgmxs;
48 12         104 require $path;
49              
50 12 50       44 my $factory_name = delete($args{'-factory'})
51             or croak 'undefined factory name';
52 12 50       137 my $factory = $package->can($factory_name)
53             or croak "no factory '$factory_name' in package '$package'";
54              
55             # expand sub-arguments under '-recursed'
56 12 50       43 if (my $r = delete($args{'-recursed'})) {
57 0         0 $args{$_} = [map { __SUB__->($_) } $r->{$_}->@*] for keys $r->%*;
  0         0  
58             }
59              
60 12         44 return $factory->(%args);
61             } ## end sub generate
62              
63 9     9   114 sub __dir_tree ($root, $path) {
  9         13  
  9         11  
  9         11  
64             return [
65             map {
66 9 100       30 $_->is_dir
  13         2442  
67             ? __SUB__->($root, $_)->@*
68             : $_->relative($root)->stringify,
69             } $path->children
70             ];
71             } ## end sub __dir_tree
72              
73 83     83 1 118 sub resolved ($throw, $value, $meta, @rest) {
  83         138  
  83         121  
  83         145  
  83         114  
  83         102  
74 83 100       269 $meta = {$meta, @rest} if @rest;
75 83 100 50     527 die $meta if $throw && ($meta->{type} // '') eq 'error';
      100        
76 56 100       204 return ($value, $meta) if wantarray;
77 41         271 return $value;
78             } ## end sub resolved
79              
80 53     53 1 109 sub resolved_error ($throw, $code, $message, @rest) {
  53         95  
  53         70  
  53         82  
  53         89  
  53         123  
81 53 50       141 my %meta = @rest == 1 ? $rest[0]->%* : @rest;
82 53         282 %meta = (type => 'error', code => $code, message => $message, %meta);
83 53         148 return resolved($throw, undef, \%meta);
84             }
85              
86 53     53 1 16303 sub resolved_error_factory ($t) { return sub { resolved_error($t, @_) } }
  24     24   37  
  24         44  
  24         30  
  24         104  
87 30     30 1 1145 sub resolved_factory ($throw) { return sub { resolved($throw, @_) } }
  24     24   49  
  24         60  
  24         34  
  24         106  
88              
89 4     4 1 9 sub resolver_from_alternatives (@args) {
  4         10  
  4         7  
90 4 50 33     32 my %args = @args && ref($args[0]) ? $args[0]->%* : @args;
91             my @alts =
92 4 50       21 map { ref($_) eq 'CODE' ? $_ : generate($_) } $args{alternatives}->@*;
  8         73  
93 4         19 my $OK = resolved_factory($args{throw});
94 4         15 my $NO = resolved_error_factory($args{throw});
95 16     16   26883 return sub ($key, @type) {
  16         37  
  16         32  
  16         26  
96 16 100 50     105 if (@type && ($type[0] // '') eq 'list') {
      100        
97 6 100       22 return $NO->(400, 'Unsupported listing in sub-directory')
98             if defined($key);
99 2         5 my %seen;
100 8         26 my @list = grep { !$seen{$_}++ }
101 4         12 map { $_->@* }
102 4         12 grep { defined($_) }
103 2         5 map { scalar eval { $_->(undef, 'list') } } @alts;
  4         8  
  4         9  
104 2         9 return $OK->(\@list, type => 'list');
105             } ## end if ($type eq 'list')
106              
107 10         26 for my $candidate (@alts) {
108 17         30 my @retval;
109 17 100       36 eval { @retval = $candidate->($key, @type) } or next;
  17         53  
110 10 100       48 return $OK->(@retval) if defined $retval[0];
111             }
112 6         18 return $NO->(404, 'Not Found');
113 4         48 };
114             } ## end sub resolver_from_alternatives
115              
116 8     8 1 251911 sub resolver_from_dir (@args) {
  8         22  
  8         13  
117 8 50 33     61 my %args = @args && ref($args[1]) ? $args[1]->%* : @args;
118 8         47 require Path::Tiny;
119 8   33     45 my $root = Path::Tiny::path($args{root} // $args{path})->realpath;
120 21     21   27 my $get = sub ($key) {
  21         27  
  21         25  
121 21         27 my $candidate = eval { $root->child($key)->realpath };
  21         71  
122 21 100 66     5085 return $candidate
      66        
123             if $candidate
124             && $candidate->exists
125             && $root->subsumes($candidate);
126 8         123 return undef;
127 8         2191 };
128 8         52 my $OK = resolved_factory($args{throw});
129 8         32 my $NO = resolved_error_factory($args{throw});
130 25     25   15847 return sub ($key, $type = 'file') {
  25         39  
  25         36  
  25         29  
131 25 100       85 if ($type eq 'list') {
132 9 100       25 my $l_root = defined($key) ? $get->($key) : $root;
133 9 100       250 return $NO->(404, 'Not Found') unless defined $l_root;
134 7 100       18 return $NO->(400, 'Not a container') unless $l_root->is_dir;
135 5         128 return $OK->(__dir_tree($root, $l_root), type => 'list');
136             } ## end if ($type eq 'list')
137              
138 16         36 my $path = $get->($key);
139 16 100       1118 return $NO->(404, 'Not Found') unless defined $path;
140 10 100       36 return $NO->(400, 'Not a file') unless $path->is_file;
141 8         180 my $ref = transform($path->stringify, file => $type);
142 8 100       34 return $NO->(400, "Invalid request type '$type'") unless $ref;
143 4         10 return $OK->($$ref, type => $type);
144             }
145 8         91 } ## end sub resolver_from_dir
146              
147 0     0 1 0 sub resolver_from_passthrough (@args) {
  0         0  
  0         0  
148 0 0 0     0 my %args = @args && ref($args[1]) ? $args[1]->%* : @args;
149 0     0   0 return sub ($key, $type = undef) {
  0         0  
  0         0  
  0         0  
150 0         0 return resolved($key, type => $type, %args);
151             }
152 0         0 } ## end sub resolver_from_passthrough
153              
154 12     12 1 436534 sub resolver_from_tar (@args) {
  12         38  
  12         18  
155 12 50 33     97 my %args = @args && ref($args[1]) ? $args[1]->%* : @args;
156 12         2732 require Archive::Tar;
157 12         368244 my $tar = Archive::Tar->new;
158 12   33     207 $tar->read($args{archive} // $args{path});
159 12         76190 my $OK = resolved_factory($args{throw});
160 12         54 my $NO = resolved_error_factory($args{throw});
161 42     42   39108 my $get = sub ($key, $type = 'data') {
  42         82  
  42         81  
  42         77  
162 42 100       124 if ($type eq 'list') {
163 14 100       45 return $NO->(400, 'Unsupported listing in sub-directory')
164             if defined($key);
165 6         24 return $OK->([grep { !m{/$} } $tar->list_files], type => 'list');
  20         643  
166             }
167              
168 28         86 $key = $key =~ s{\A \./}{}rmxs;
169 28 100       179 $key = './' . $key unless $tar->contains_file($key);
170 28 100       23607 return $NO->(404, 'Not Found') unless $tar->contains_file($key);
171 15         1262 my $ref = transform($tar->get_content($key), data => $type);
172 15 100       63 return $NO->(400, "Invalid request type '$type'") unless $ref;
173 9         28 return $OK->($$ref, type => $type);
174 12         126 };
175             } ## end sub resolver_from_tar
176              
177             # ----------------------------------------------------------------------
178             # Transformers
179              
180 3 50   3 1 21 sub data_to_fh { file_to_fh(ref($_[0]) ? $_[0] : \$_[0]) }
181              
182             sub data_to_file {
183 3   50 3 1 22 my $keep = $_[1] // 0;
184 3         3008 require File::Temp;
185 3         33903 my ($fh, $filename) = File::Temp::tempfile(UNLINK => (!$keep));
186 3         2452 binmode $fh, ':raw';
187 3 50       7 (print {$fh} ref($_[0]) ? ${$_[0]} : $_[0]) or croak "print: $OS_ERROR";
  3 50       34  
  0         0  
188 3 50       172 close $fh or croak "close: $OS_ERROR";
189 3         20 return $filename;
190             } ## end sub data_to_file
191              
192 2     2 1 3 sub fh_to_data ($fh) { local $/; readline($fh) }
  2         5  
  2         4  
  2         9  
  2         82  
193              
194 0     0 1 0 sub fh_to_file ($fh, $keep = 0) { data_to_file(fh_to_data($fh), $keep) }
  0         0  
  0         0  
  0         0  
  0         0  
195              
196 2     2 1 4 sub file_to_data ($input) { fh_to_data(file_to_fh($input)) }
  2         5  
  2         3  
  2         6  
197              
198 6     6 1 19 sub file_to_fh ($input) {
  6         11  
  6         21  
199 6 50       173 open my $fh, '<:raw', $input or croak "open('$input'): $OS_ERROR";
200 6         24 return $fh;
201             }
202              
203             sub transform {
204 23     23 1 1361 state $canonical_name_for = {
205             fh => 'filehandle',
206             filehandle => 'filehandle',
207             data => 'data',
208             file => 'file',
209             path => 'file',
210             };
211 23 50       91 my $itype = $canonical_name_for->{$_[1]} or return;
212 23 100       83 my $otype = $canonical_name_for->{$_[2]} or return;
213              
214 13 100       72 return \$_[0] if $itype eq $otype;
215              
216 9         59 state $transformer_for = { # itype, otype
217             file => {
218             fh => \&file_to_fh,
219             filehandle => \&file_to_fh,
220             data => \&file_to_data,
221             },
222             filehandle => {
223             data => \&fh_to_data,
224             file => \&fh_to_file,
225             path => \&fh_to_file,
226             },
227             data => {
228             fh => \&data_to_fh,
229             filehandle => \&data_to_fh,
230             file => \&data_to_file,
231             path => \&data_to_file,
232             },
233             };
234 9         38 my $value = $transformer_for->{$itype}{$otype}->($_[0]);
235 9         51 return \$value;
236             } ## end sub transform
237              
238             1;