File Coverage

blib/lib/YATT/Lite/Factory.pm
Criterion Covered Total %
statement 300 411 72.9
branch 72 138 52.1
condition 39 87 44.8
subroutine 67 88 76.1
pod 3 55 5.4
total 481 779 61.7


line stmt bran cond sub pod time code
1             package YATT::Lite::Factory;
2 7     7   18591 use strict;
  7         12  
  7         226  
3 7     7   38 use warnings qw(FATAL all NONFATAL misc);
  7         16  
  7         268  
4 7     7   37 use Carp;
  7         12  
  7         414  
5 7     7   41 use YATT::Lite::Breakpoint;
  7         13  
  7         543  
6             sub MY () {__PACKAGE__}
7              
8 7     7   403 use constant DEBUG_FACTORY => $ENV{DEBUG_YATT_FACTORY};
  7         14  
  7         469  
9              
10 7     7   136 use 5.010;
  7         24  
11 7     7   37 use Scalar::Util qw(weaken);
  7         14  
  7         383  
12 7     7   128 use Encode qw/decode/;
  7         25558  
  7         577  
13              
14 7     7   38 use parent qw/File::Spec YATT::Lite::NSBuilder/;
  7         14  
  7         52  
15 7     7   353 use File::Path ();
  7         16  
  7         156  
16 7     7   40 use File::Basename qw/dirname/;
  7         14  
  7         458  
17              
18 7     7   2949 use YATT::Lite::PSGIEnv;
  7         17  
  7         41  
19              
20             # Note: Definition of default values are not yet gathered here.
21             # Some are in YATT::Lite, others are in YATT::Lite::Core, CGen.. and so on.
22              
23             use YATT::Lite::MFields
24 7         147 ([cf_namespace =>
25             (doc => "namespace prefix for yatt. (default: [yatt, perl])")]
26              
27             , [cf_doc_root =>
28             (doc => "Primary template directory")]
29              
30             , [cf_app_base =>
31             (doc => "Base dir for this siteapp")]
32              
33             , [cf_site_prefix =>
34             (doc => "Location prefix for this siteapp")]
35              
36             , [cf_index_name =>
37             (doc => "Rootname of index template. (default: index)")]
38              
39             , [cf_ext_public =>
40             (doc => "public file extension for yatt. (default: yatt)")]
41              
42             , [cf_ext_private =>
43             (doc => "hidden file extension for yatt. (default: ytmpl)")]
44              
45             , [cf_header_charset =>
46             (doc => "Charset for outgoing HTTP Content-Type. (default: utf-8)")]
47              
48             , [cf_tmpl_encoding =>
49             (doc => "Perl encoding used while reading yatt templates. (default: 'utf-8')")]
50              
51             , [cf_output_encoding =>
52             (doc => "Perl encoding used for outgoing response body."
53             ." Also this is used to decode incoming request parameters and PATH_INFO."
54             ." (default: 'utf-8')")]
55              
56             , [cf_offline =>
57             (doc => "Whether header should be emitted or not.")]
58              
59             , [cf_binary_config =>
60             (doc => "(This may be changed in future release) Whether .htyattconfig.* should be read with encoding or not.")]
61              
62             , [cf_no_unicode =>
63             (doc => "(Compatibility option) Avoid use of utf8.")]
64              
65             , [cf_no_unicode_params =>
66             (doc => "(Compatibility option) Avoid encoding conversion of input params.")]
67              
68             , [cf_use_subpath =>
69             (doc => "pass sub-path_info")]
70              
71             , qw/
72             cf_allow_missing_dir
73              
74             tmpldirs
75             loc2yatt
76             path2yatt
77              
78             loc2psgi_re
79             loc2psgi_dict
80              
81             tmpl_cache
82              
83             path2entns
84              
85             cf_debug_cgen
86              
87             cf_only_parse
88             cf_config_filetypes
89              
90             cf_dont_map_args
91             cf_dont_debug_param
92             cf_always_refresh_deps
93 7     7   41 /);
  7         15  
94              
95 7     7   4454 use YATT::Lite::Util::AsBase;
  7         17  
  7         51  
96 7         731 use YATT::Lite::Util qw/lexpand globref untaint_any ckrequire dofile_in
97             lookup_dir fields_hash
98             lookup_path
99             secure_text_plain
100             psgi_error
101             globref_default
102             define_const
103             terse_dump
104 7     7   96 /;
  7         13  
105              
106 7     7   38 use YATT::Lite::XHF ();
  7         13  
  7         157  
107              
108 7     7   2420 use YATT::Lite::Partial::ErrorReporter;
  7         17  
  7         64  
109 7     7   2329 use YATT::Lite::Partial::AppPath;
  7         16  
  7         42  
110              
111 7     7   2542 use YATT::Lite qw/Entity *SYS *YATT *CON/;
  7         16  
  7         74  
112              
113              
114 7     7   4296 use YATT::Lite::Util::CycleDetector qw/Visits/;
  7         17  
  7         30308  
115              
116             #========================================
117             #
118             #
119             #
120              
121             our $want_object;
122 1     1 0 5 sub want_object { $want_object }
123              
124             sub find_load_factory_script {
125 0     0 0 0 my ($pack, %opts) = @_;
126             my ($found) = $pack->find_factory_script(delete $opts{dir})
127 0 0       0 or return;
128 0 0       0 my $self = $pack->load_factory_script($found)
129             or croak "Can't load YATT::Lite::Factory instance from $found";
130 0         0 $self->configure(%opts);
131 0         0 $self;
132             }
133              
134             sub load_factory_offline {
135 0     0 0 0 shift->find_load_factory_script(offline => 1, @_);
136             }
137              
138             sub configure_offline {
139 0     0 0 0 (my MY $self, my $value) = @_;
140 0         0 $self->{cf_offline} = $value;
141 0 0       0 if ($self->{cf_offline}) {
142             $self->configure(error_handler => sub {
143 0     0   0 my ($type, $err) = @_;
144 0         0 die $err;
145             })
146 0         0 }
147             }
148              
149             #========================================
150              
151             sub load_factory_for_psgi {
152 0     0 0 0 my ($pack, $psgi, %default) = @_;
153 0 0       0 unless (defined $psgi) {
154 0         0 croak "Usage: Factory->load_factory_for_psgi(psgi_filename, \%opts)";
155             }
156 0 0       0 unless (-r $psgi) {
157 0         0 croak "psgi is not readable: $psgi";
158             }
159 0         0 (my $app_rootname = $pack->rel2abs($psgi)) =~ s/\.psgi$//;
160              
161             #
162             # Assume app_root is safe.
163             #
164 0         0 my $app_root = untaint_any(dirname($app_rootname));
165 0 0       0 unless (-d $app_root) {
166 0         0 croak "Can't find app_root for $psgi";
167             }
168              
169 0   0     0 $default{doc_root} ||= "$app_root/html";
170 0 0       0 if (-d "$app_root/ytmpl") {
171 0   0     0 $default{app_base} ||= '@ytmpl';
172             }
173 0 0       0 if (my (@cf) = map {
174 0         0 my $cf = "$app_rootname.$_";
175 0 0       0 -e $cf ? $cf : ()
176             } $pack->default_config_filetypes) {
177 0 0       0 croak "Multiple configuration files!: @cf" if @cf > 1;
178             $pack->_with_loading_file($cf[0], sub {
179 0     0   0 $pack->new(app_root => $app_root, %default
180             , $pack->read_file($cf[0]));
181             })
182 0         0 } else {
183 0         0 $pack->new(app_root => $app_root, %default);
184             }
185             }
186              
187             #========================================
188              
189             {
190             my %sub2app;
191             sub to_app {
192 4     4 0 12 my ($self, $cascade, @fallback) = @_;
193 4         18 $self->prepare_app;
194 4     37   38 my $sub = sub { $self->call(@_) };
  37         27848  
195 4         18 $sub2app{$sub} = $self; weaken($sub2app{$sub});
  4         26  
196 4 50       17 if ($cascade) {
197 0         0 $sub2app{$cascade} = $self; weaken($sub2app{$cascade});
  0         0  
198 0         0 $cascade->add($sub, @fallback);
199 0         0 $cascade->to_app;
200             } else {
201 4         30 $sub;
202             }
203             }
204             sub wrapped_by {
205 0     0 0 0 my ($self, $builder) = @_;
206 0         0 my $outer_app = $builder->($self->to_app);
207 0         0 $sub2app{$outer_app} = $self; weaken($sub2app{$outer_app});
  0         0  
208 0         0 $outer_app;
209             }
210             sub load_psgi_script {
211 1     1 0 3 my ($pack, $fn) = @_;
212 1         2 local $want_object = 1;
213 1         8 local $0 = $fn;
214 1         5 my $sub = $pack->sandbox_dofile($fn);
215 1 50 33     16 if (ref $sub eq 'CODE') {
    50          
216 0         0 $sub2app{$sub};
217             } elsif ($sub->isa($pack) or $sub->isa(MY)) {
218 1         26 $sub;
219             } else {
220 0         0 die "Unknown load result from: $fn";
221             }
222             }
223 0     0 0 0 sub prepare_app { return }
224              
225             our $load_count;
226             sub sandbox_dofile {
227 1     1 0 2 my ($pack, $file) = @_;
228 1         6 my $sandbox = sprintf "%s::Sandbox::S%d", __PACKAGE__, ++$load_count;
229 1         2 my @__result__;
230 1 50       3 if (wantarray) {
231 0         0 @__result__ = dofile_in($sandbox, $file);
232             } else {
233 1         7 $__result__[0] = dofile_in($sandbox, $file);
234             }
235 1         6 my $sym = globref($sandbox, 'filename');
236 1 50       3 unless (*{$sym}{CODE}) {
  1         4  
237 1     0   6 *$sym = sub {$file};
  0         0  
238             }
239 1 50       6 wantarray ? @__result__ : $__result__[0];
240             }
241             }
242              
243             sub load_factory_script {
244 1     1 0 11 my ($pack, $fn) = @_;
245 1         4 local $want_object = 1;
246 1         23 local $0 = $fn;
247 1         4 local ($FindBin::Bin, $FindBin::Script
248             , $FindBin::RealBin, $FindBin::RealScript);
249 1 50       22 FindBin->again if FindBin->can("again");
250 1 50       154 if ($fn =~ /\.psgi$/) {
251 1         7 $pack->load_psgi_script($fn);
252             } else {
253 0         0 $pack->sandbox_dofile($fn);
254             }
255             }
256              
257             sub find_factory_script {
258 0     0 0 0 my $pack = shift;
259 0   0     0 my $dir = $pack->rel2abs($_[0] // $pack->curdir);
260 0         0 my @path = $pack->no_upwards($pack->splitdir($dir));
261 0         0 my $rootdir = $pack->rootdir;
262 0   0     0 while (@path and length($dir = $pack->catdir(@path)) > length($rootdir)) {
263 0 0       0 if (my ($found) = grep {-r} map {"$dir/$_.psgi"} qw(runyatt app)) {
  0         0  
  0         0  
264 0         0 return $found;
265             }
266 0         0 } continue { pop @path }
267 0         0 return;
268             }
269              
270             #========================================
271              
272             sub init_app_ns {
273 18     18 0 48 (my MY $self) = @_;
274 18         112 $self->SUPER::init_app_ns;
275 18         84 $self->{default_app}->ensure_entns($self->{app_ns});
276             }
277              
278             sub after_new {
279 18     18 1 36 (my MY $self) = @_;
280 18         91 $self->SUPER::after_new;
281 18   33     144 $self->{cf_index_name} //= $self->default_index_name;
282 18   33     133 $self->{cf_ext_public} //= $self->default_ext_public;
283 18   33     131 $self->{cf_ext_private} //= $self->default_ext_private;
284 18 50       58 if ($self->{cf_no_unicode}) {
285 0         0 $self->{cf_no_unicode_params} = 1;
286 0         0 $self->{cf_binary_config} = 1;
287             $self->{cf_header_charset}
288 0   0     0 //= ($self->{cf_output_encoding} || $self->default_header_charset);
      0        
289             $self->{cf_output_encoding}
290 0   0     0 //= $self->compat_default_output_encoding;
291             } else {
292             $self->{cf_header_charset}
293 18   66     132 //= ($self->{cf_output_encoding} // $self->default_header_charset);
      66        
294             $self->{cf_tmpl_encoding}
295 18   66     144 //= ($self->{cf_output_encoding} // $self->default_tmpl_encoding);
      66        
296 18   66     103 $self->{cf_output_encoding} //= $self->default_output_encoding;
297             }
298 18   100     118 $self->{cf_use_subpath} //= 1;
299             }
300              
301 0     0 0 0 sub compat_default_output_encoding { '' }
302 16     16 0 54 sub default_output_encoding { 'utf-8' }
303 15     15 0 65 sub default_header_charset { 'utf-8' }
304 16     16 0 76 sub default_tmpl_encoding { 'utf-8' }
305 18     18 0 67 sub default_index_name { 'index' }
306 18     18 0 57 sub default_ext_public {'yatt'}
307 18     18 0 63 sub default_ext_private {'ytmpl'}
308              
309             sub _after_after_new {
310 18     18   59 (my MY $self) = @_;
311 18         86 $self->SUPER::_after_after_new;
312              
313 18 50 33     592 if (not $self->{cf_allow_missing_dir}
      33        
314             and $self->{cf_doc_root}
315             and not -d $self->{cf_doc_root}) {
316 0         0 croak "document_root '$self->{cf_doc_root}' is missing!";
317             }
318 18 50       75 if ($self->{cf_doc_root}) {
319 18         65 trim_slash($self->{cf_doc_root});
320             }
321             # XXX: $self->{cf_tmpldirs}
322              
323 18   100     100 $self->{cf_site_prefix} //= "";
324              
325 18         51 $self->{tmpldirs} = [];
326 18 50       64 if (my $dir = $self->{cf_doc_root}) {
327 18         34 push @{$self->{tmpldirs}}, $dir;
  18         57  
328 18         89 $self->get_yatt('/');
329             }
330             }
331              
332             #========================================
333              
334             sub render {
335 0     0 1 0 my MY $self = shift;
336 0         0 my $raw_bytes = $self->render_encoded(@_);
337 0         0 decode(utf8 => $raw_bytes);
338             }
339              
340             sub render_encoded {
341 0     0 0 0 (my MY $self, my ($reqrec, $args, @opts)) = @_;
342             # [$path_info, $subpage, $action]
343 0 0       0 my ($path_info, @rest) = ref $reqrec ? @$reqrec : $reqrec;
344              
345 0         0 $path_info =~ s,^/*,/,;
346              
347 0         0 my ($tmpldir, $loc, $file, $trailer, $is_index)
348             = my @pi = $self->lookup_split_path_info($path_info);
349 0 0       0 unless (@pi) {
350 0         0 die "No such location: $path_info";
351             }
352              
353 0 0       0 my $dh = $self->get_lochandler(map {untaint_any($_)} $loc, $tmpldir) or do {
  0         0  
354 0         0 die "No such directory: $path_info";
355             };
356              
357 0         0 my $con = $self->make_simple_connection
358             (
359             \@pi, yatt => $dh, noheader => 1, path_info => $path_info
360             , $self->make_debug_params($reqrec, $args)
361             );
362              
363 0 0       0 $self->invoke_dirhandler
364             (
365             $dh, $con
366             , render_into => $con
367             , @rest ? [$file, @rest] : $file
368             , $args, @opts
369             );
370              
371 0         0 $con->buffer;
372             }
373              
374             sub lookup_split_path_info {
375 10     10 0 20 (my MY $self, my $path_info) = @_;
376             lookup_path($path_info
377             , $self->{tmpldirs}
378             , $self->{cf_index_name}, ".$self->{cf_ext_public}"
379 10         72 , $self->{cf_use_subpath});
380             }
381              
382             #========================================
383              
384             sub K_MOUNT_MATCH () { "__yatt" }
385              
386             sub lookup_psgi_mount {
387 22     22 0 46 (my MY $self, my $path_info) = @_;
388 22   66     79 $self->{loc2psgi_re} // $self->rebuild_psgi_mount;
389             $path_info =~ $self->{loc2psgi_re}
390 22 100       213 or return;
391 3 50   3   170 my @mount_match = grep {/^@{[K_MOUNT_MATCH()]}/o} keys %+
  3         3742  
  3         10666  
  3         27  
  3         42  
  1         22  
392             or return;
393 3 50       10 if (@mount_match >= 2) {
394             croak "Multiple match found for psgi_mount: \n"
395 0         0 . join("\n ", map {$self->{loc2psgi_dict}{$_}[0]} @mount_match);
  0         0  
396             }
397              
398 3         30 my $path_prefix = $+{$mount_match[0]};
399              
400 3         11 my $item = $self->{loc2psgi_dict}{$path_prefix};
401              
402 3 50       27 wantarray ? @{$item}[1..$#$item] : $item->[2];
  0         0  
403             }
404              
405             sub mount_psgi {
406 3     3 0 2299 (my MY $self, my ($path_prefix, $app, @opts)) = @_;
407 3 50       12 unless (defined $path_prefix) {
408 0         0 croak "path_prefix is empty! mount_psgi(path_prefix, psgi_app)";
409             }
410 3 50       10 if (not ref $path_prefix) {
411 3         18 $path_prefix =~ s,^/*,/,;
412             }
413 3   100     17 my $dict = $self->{loc2psgi_dict} //= +{};
414 3         10 my $key = K_MOUNT_MATCH() . (keys %$dict);
415 3         11 $dict->{$path_prefix} = [$key => $path_prefix => $app, @opts];
416              
417 3         13 undef $self->{loc2psgi_re};
418              
419             # For cascading call
420 3         8 $self;
421             }
422              
423             sub rebuild_psgi_mount {
424 3     3 0 5 (my MY $self) = @_;
425 3         4 my @re;
426 3   50     5 foreach my $path_prefix (keys %{$self->{loc2psgi_dict} //= +{}}) {
  3         46  
427 5         7 my ($key, undef, $app) = @{$self->{loc2psgi_dict}{$path_prefix}};
  5         14  
428 5         93 push @re, qr{(?<$key>$path_prefix)};
429             }
430 3         9 my $all = join("|", @re);
431 3         73 $self->{loc2psgi_re} = qr{^(?:$all)(?:/|$)};
432             }
433              
434             sub psgi_file_app {
435 2     2 0 82 my ($pack, $path) = @_;
436 2         61 require Plack::App::File;
437 2         21467 Plack::App::File->new(root => $path)->to_app;
438             }
439              
440             sub mount_static {
441 0     0 0 0 (my MY $self, my ($location, $realpath)) = @_;
442 0 0       0 my $app = ref $realpath eq 'CODE' ? $realpath
443             : $self->psgi_file_app($realpath);
444             $self->mount_psgi
445             ($location, sub {
446 0     0   0 (my Env $env) = @_;
447 0         0 local $env->{PATH_INFO} = _trim_prefix($env->{PATH_INFO}, $location);
448 0         0 $app->($env);
449 0         0 });
450             }
451              
452             sub _trim_prefix {
453 0     0   0 substr($_[0], length($_[1]));
454             }
455              
456              
457             #========================================
458              
459             sub mount_action {
460 10     10 0 156995 (my MY $self, my ($path_info, $action)) = @_;
461 10 50       48 if (my $ref = ref $path_info) {
462 0         0 croak "mount_action doesn't support $ref path_info, sorry";
463             }
464 10         46 my ($tmpldir, $loc, $file, $trailer, $is_index)
465             = my @pi = $self->lookup_split_path_info($path_info);
466 10 50       53 unless (@pi) {
467 0         0 croak "Can't find acutal directory for $path_info";
468             }
469 10 50       29 unless ($is_index) {
470 0         0 croak "Conflicting mount_action($path_info) with file=$file\n";
471             }
472 10         25 my $realdir = $tmpldir.$loc;
473 10         49 my $dh = $self->get_dirhandler($realdir);
474 10         89 $dh->set_action_handler($trailer, $action);
475              
476             # For cascading call.
477 10         56 $self;
478             }
479              
480             #========================================
481              
482             sub Connection () {'YATT::Lite::Connection'};
483              
484             sub make_simple_connection {
485 0     0 0 0 (my MY $self, my ($quad, @rest)) = @_;
486 0         0 my ($tmpldir, $loc, $file, $trailer) = @$quad;
487 0         0 my $virtdir = "$self->{cf_doc_root}$loc";
488 0         0 my $realdir = "$tmpldir$loc";
489 0         0 my @params = $self->connection_quad([$virtdir, $loc, $file, $trailer]);
490 0         0 $self->make_connection(undef, @params, @rest);
491             }
492              
493             sub make_debug_params {
494 0     0 0 0 (my MY $self, my ($reqrec, $args)) = @_;
495 0         0 ();
496             }
497              
498             sub make_connection {
499 59     59 0 437 (my MY $self, my ($fh, @params)) = @_;
500 59         430 require YATT::Lite::Connection;
501             $self->Connection->create(
502             $fh, @params, system => $self, root => $self->{cf_doc_root}
503 59         586 );
504             }
505              
506       0 0   sub finalize_connection {}
507              
508             sub connection_param {
509 0     0 0 0 croak "Use of YATT::Lite::Factory::connection_param is deprecated!\n";
510             }
511             sub connection_quad {
512 45     45 0 85 (my MY $self, my ($quad)) = @_;
513 45         102 my ($virtdir, $loc, $file, $subpath) = @$quad;
514 45         420 (dir => $virtdir
515             , location => $loc
516             , file => $file
517             , subpath => $subpath);
518             }
519              
520             #========================================
521             #
522             # Hook for subclassing
523             #
524             sub run_dirhandler {
525 45     45 0 120 (my MY $self, my ($dh, $con, $file)) = @_;
526 45         156 local ($SYS, $YATT, $CON) = ($self, $dh, $con);
527 45         158 $self->before_dirhandler($dh, $con, $file);
528 45         815 $self->invoke_dirhandler($dh, $con
529             , handle => $dh->cut_ext($file), $con, $file);
530 41         151 $self->after_dirhandler($dh, $con, $file);
531             }
532              
533 29     29 0 88 sub before_dirhandler { &maybe::next::method; }
534 41     41 0 128 sub after_dirhandler { &maybe::next::method; }
535              
536             sub invoke_dirhandler {
537 45     45 0 189 (my MY $self, my ($dh, $con, $method, @args)) = @_;
538 45         225 $dh->with_system($self, $method, @args);
539             }
540              
541             #========================================
542              
543             sub get_lochandler {
544 31     31 0 75 (my MY $self, my ($location, $tmpldir)) = @_;
545 31   66     103 $tmpldir //= $self->{cf_doc_root};
546 31 50       131 $self->get_yatt($location) || do {
547 0         0 $self->{loc2yatt}{$location} = $self->load_yatt("$tmpldir$location");
548             };
549             }
550              
551             # location => yatt (dirhandler, dirapp)
552              
553             sub get_yatt {
554 58     58 1 2272 (my MY $self, my $loc) = @_;
555 58 50       223 if (my $yatt = $self->{loc2yatt}{$loc}) {
556 0         0 return $yatt;
557             }
558             # print STDERR Carp::longmess("get_yatt for $loc"
559             # , YATT::Lite::Util::terse_dump($self->{tmpldirs}));
560 58         159 my ($realdir, $basedir) = lookup_dir(trim_slash($loc), $self->{tmpldirs});
561 58 50       212 unless ($realdir) {
562 0         0 $self->error("Can't find template directory for location '%s'", $loc);
563             }
564 58         206 $self->{loc2yatt}{$loc} = $self->load_yatt($realdir, $basedir);
565             }
566              
567             # phys-path => yatt
568              
569             sub load_yatt {
570 89     89 0 1516 (my MY $self, my ($path, $basedir, $visits, $from)) = @_;
571 89         1155 $path = $self->rel2abs($path, $self->{cf_app_root});
572 89 100       395 if (my $yatt = $self->{path2yatt}{$path}) {
573 53         388 return $yatt;
574             }
575 36 100       129 if (not $visits) {
    100          
576 23         226 $visits = Visits->start($path);
577             } elsif (my $preds = $visits->check_cycle($path, $from)) {
578 1         6 $self->error("Template config error! base has cycle!:\n %s\n"
579             , join "\n -> ", $from, @$preds);
580             }
581             #-- DFS-visits --
582 35 50 33     735 if (not $self->{cf_allow_missing_dir} and not -d $path) {
583 0         0 croak "Can't find '$path'!";
584             }
585 35 100       165 if (my (@cf) = map {
586 70         226 my $cf = untaint_any($path) . "/.htyattconfig.$_";
587 70 100       1799 -e $cf ? $cf : ()
588             } $self->config_filetypes) {
589 13 50       41 $self->error("Multiple configuration files!", @cf) if @cf > 1;
590 13         112 _with_loading_file {$self} $cf[0], sub {
591 13     13   38 $self->build_yatt($path, $basedir, $visits, $self->read_file($cf[0]));
592 13         99 };
593             } else {
594 22         127 $self->build_yatt($path, $basedir, $visits);
595             }
596             }
597              
598             sub build_yatt {
599 35     35 0 137 (my MY $self, my ($path, $basedir, $visits, %opts)) = @_;
600 35         102 trim_slash($path);
601              
602 35         133 my $app_name = $self->app_name_for($path, $basedir);
603              
604             #
605             # base package と base vfs object の決定
606             #
607 35         60 my (@basepkg, @basevfs);
608 35         240 $self->_list_base_spec_in($path, delete $opts{base}, $visits
609             , \@basepkg, \@basevfs);
610              
611 32         186 my $app_ns = $self->buildns(my @log = (INST => \@basepkg, $path));
612              
613 32         52 print STDERR "# Factory::buildns("
614             , terse_dump(@log), ") => $app_ns\n" if DEBUG_FACTORY;
615              
616 32 100       910 if (-e (my $rc = "$path/.htyattrc.pl")) {
617             # Note: This can do "use fields (...)"
618 4         19 dofile_in($app_ns, $rc);
619             }
620              
621             my @args = (vfs => [dir => $path
622             , entns => $self->{path2entns}{$path}
623             , encoding => $self->{cf_tmpl_encoding}
624             , @basevfs ? (base => \@basevfs) : ()]
625             , dir => $path
626             , app_ns => $app_ns
627             , app_name => $app_name
628             , factory => $self
629              
630             # XXX: Design flaw! Use of tmpl_cache will cause problem.
631             # because VFS->create for base do not respect Factory->get_yatt.
632             # To solve this, I should redesign all Factory/VFS related stuffs.
633             , tmpl_cache => $self->{tmpl_cache} //= {}
634              
635 32 100 100     400 , $self->configparams_for(fields_hash($app_ns)));
636              
637 32 50       260 if (my @unk = $app_ns->YATT::Lite::Object::cf_unknowns(%opts)) {
638 0         0 $self->error("Unknown option for yatt app '%s': '%s'"
639             , $path, join(", ", @unk));
640             }
641              
642 32         265 $self->{path2yatt}{$path} = $app_ns->new(@args, %opts);
643             }
644              
645             sub _list_base_spec_in {
646 35     35   119 (my MY $self, my ($in, $desc, $visits, $basepkg, $basevfs)) = @_;
647              
648             print STDERR "# Factory::list_base_in("
649 35         60 , terse_dump($in, $desc, $self->{cf_app_base}), ")\n" if DEBUG_FACTORY;
650              
651 35         73 my $is_implicit = not defined $desc;
652              
653 35   100     148 $desc //= $self->{cf_app_base};
654              
655 35 100       119 my ($base, @mixin) = lexpand($desc)
656             or return;
657              
658 23         39 my @pkg_n_dir;
659 23         95 foreach my $task ([1, $base], [0, @mixin]) {
660 46         111 my ($is_primary, @spec) = @$task;
661 46         97 foreach my $basespec (@spec) {
662 27         32 my ($pkg, $yatt);
663 27 100       164 if ($basespec =~ /^::(.*)/) {
    50          
664 5         26 ckrequire($1);
665 5         27 push @pkg_n_dir, [$is_primary, $1, undef];
666             } elsif (my $realpath = $self->app_path_find_dir_in($in, $basespec)) {
667 22 100       51 if ($is_implicit) {
668 7 100       32 next if $visits->has_node($realpath);
669             }
670 18         64 $visits->ensure_make_node($realpath);
671 18         79 push @pkg_n_dir, [$is_primary, undef, $realpath];
672             } else {
673 0         0 $self->error("Invalid base spec: %s", $basespec);
674             }
675             }
676             }
677              
678 23         56 foreach my $tuple (@pkg_n_dir) {
679 23         63 my ($is_primary, $pkg, $dir) = @$tuple;
680 23 100       70 next unless $dir;
681 18         65 my $yatt = $self->load_yatt($dir, undef, $visits, $in);
682 15         40 $tuple->[1] = ref $yatt;
683 15         100 my $realdir = $yatt->cget('dir');
684 15         85 push @$basevfs, [dir => $realdir, entns => $self->{path2entns}{$realdir}];
685             }
686              
687             push @$basepkg, map {
688 20         47 my ($is_primary, $pkg, $dir) = @$_;
  20         46  
689 20 100 66     143 ($is_primary && $pkg) ? ($pkg) : ()
690             } @pkg_n_dir;
691              
692 20         81 $visits->finish_node($in);
693             }
694              
695             #========================================
696              
697             sub buildns {
698 32     32 0 94 (my MY $self, my ($kind, $baselist, $path)) = @_;
699 32         173 my $newns = $self->SUPER::buildns($kind, $baselist, $path);
700              
701             # EntNS を足し、Entity も呼べるようにする。
702             $self->{default_app}->define_Entity(undef, $newns
703 32         171 , map {$_->EntNS} @$baselist);
  16         99  
704              
705             # instns には MY を定義しておく。
706 32         110 my $my = globref($newns, 'MY');
707 32 100       54 unless (*{$my}{CODE}) {
  32         105  
708 31         93 define_const($my, $newns);
709             }
710              
711             # もし $newns の EntNS が Factory(SiteApp, app.psgi) の EntNS を継承していない
712             # なら、継承する
713 32 100       483 unless ($newns->EntNS->isa($self->EntNS)) {
714 6         10 push @{globref_default(globref($newns->EntNS, 'ISA')
  6         28  
715             , [])}, $self->EntNS;
716             }
717              
718             # basevfs に entns を渡せるように。
719 32         231 $self->{path2entns}{$path} = $newns->EntNS;
720              
721 32         78 $newns;
722             }
723              
724             sub _cf_delegates {
725 32     32   265 qw(no_unicode
726             no_unicode_params
727             output_encoding
728             header_charset
729             tmpl_encoding
730             debug_cgen
731             at_done
732             app_root
733             namespace
734             index_name
735             ext_public
736             ext_private
737             only_parse
738             use_subpath
739             dont_map_args
740             dont_debug_param
741             always_refresh_deps
742             );
743             }
744              
745             sub configparams_for {
746 32     32 0 54 (my MY $self, my $hash) = @_;
747             # my @base = map { [dir => $_] } lexpand($self->{cf_tmpldirs});
748             # (@base ? (base => \@base) : ())
749             (
750             $self->cf_delegate_known(0, $hash, $self->_cf_delegates)
751             , (exists $hash->{cf_error_handler}
752 32 50       117 ? (error_handler => \ $self->{cf_error_handler}) : ())
753             , die_in_error => ! YATT::Lite::Util::is_debugging());
754             }
755              
756             # XXX: Should have better interface.
757             sub error {
758 1     1 0 3 (my MY $self, my ($fmt, @args)) = @_;
759 1         472 croak sprintf $fmt, @args;
760             }
761              
762             #========================================
763              
764             sub app_name_for {
765 35     35 0 90 (my MY $self, my ($path, $basedir)) = @_;
766 35         110 ensure_slash($path);
767 35 100       85 if ($basedir) {
768 21         63 ensure_slash($basedir);
769 21   33     107 $self->_extract_app_name($path, $basedir)
770             // $self->error("Can't extract app_name path=%s, base=%s"
771             , $path, $basedir);
772             } else {
773 14         54 foreach my $tmpldir (lexpand($self->{tmpldirs})) {
774 14         39 ensure_slash(my $cp = $tmpldir);
775 14 100       37 if (defined(my $app_name = $self->_extract_app_name($path, $cp))) {
776             # Can be empty string.
777 5         17 return $app_name;
778             }
779             }
780 9         26 return '';
781             }
782             }
783              
784             sub _extract_app_name {
785 38     38   1508 (my MY $self, my ($path, $basedir)) = @_;
786 38         243 my ($bs, $name) = unpack('A'.length($basedir).'A*', $path);
787 38 100       163 return undef unless $bs eq $basedir;
788 28         75 $name =~ s{[/\\]+$}{};
789 28         129 $name;
790             }
791              
792             #========================================
793              
794             sub read_file {
795 15     15 0 41 (my MY $self, my $fn) = @_;
796 15 50       121 my ($ext) = $fn =~ /\.(\w+)$/
797             or croak "Can't extract fileext from filename: $fn";
798 15 50       97 my $sub = $self->can("read_file_$ext")
799             or croak "filetype $ext is not supported: $fn";
800 15         44 $sub->($self, $fn);
801             }
802              
803 35     35 0 109 sub default_config_filetypes {qw/xhf yml/}
804             sub config_filetypes {
805 35     35 0 61 (my MY $self) = @_;
806 35 50       108 if (my $item = $self->{cf_config_filetypes}) {
807 0         0 lexpand($item)
808             } else {
809 35         121 $self->default_config_filetypes
810             }
811             }
812              
813             sub read_file_xhf {
814 13     13 0 27 (my MY $self, my $fn) = @_;
815 13   66     55 my $bytes_semantics = ref $self && $self->{cf_binary_config};
816 13         85 $self->YATT::Lite::XHF::read_file_xhf
817             ($fn, bytes => $bytes_semantics);
818             }
819              
820             sub read_file_yml {
821 2     2 0 6 (my MY $self, my $fn) = @_;
822 2         14 require YAML::Tiny;
823 2         16 my $yaml = YAML::Tiny->read($fn);
824 2 50       1150 wantarray ? lexpand($yaml->[0]) : $yaml;
825             }
826              
827             #========================================
828              
829             sub trim_slash {
830 111     111 0 761 $_[0] =~ s,/*$,,;
831 111         377 $_[0];
832             }
833              
834             sub ensure_slash {
835 70 50 33 70 0 387 unless (defined $_[0] and $_[0] ne '') {
836 0         0 $_[0] = '/';
837             } else {
838 70         692 my $abs = File::Spec->rel2abs($_[0]);
839 70 50       265 my $sep = $^O =~ /^MSWin/ ? "\\" : "/";
840 70         552 $abs =~ s{(?:\Q$sep\E)?$}{$sep}; # Should end with path-separator.
841 70         221 $_[0] = $abs;
842             }
843             }
844              
845             #========================================
846             {
847             Entity site_prefix => sub {
848 0     0     my MY $self = $SYS;
849 0           $self->{cf_site_prefix};
850             };
851             }
852              
853              
854             1;