File Coverage

blib/lib/Bread/Board/LazyLoader.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Bread::Board::LazyLoader;
2             $Bread::Board::LazyLoader::VERSION = '0.13';
3 6     6   135327 use common::sense;
  6         50  
  6         28  
4              
5             # ABSTRACT: loads lazily Bread::Board containers from files
6              
7              
8 6     6   322 use Exporter 'import';
  6         10  
  6         159  
9              
10 6     6   2312 use Path::Class;
  6         116140  
  6         358  
11 6     6   3251 use Type::Params;
  6         350371  
  6         64  
12 6     6   1120 use Types::Standard qw(is_CodeRef slurpy Dict ArrayRef Str Optional CodeRef Object is_Object is_ArrayRef);
  6         8  
  6         24  
13 6     6   4882 use Carp qw(confess);
  6         7  
  6         332  
14 6     6   3965 use Moose::Meta::Role ();
  6         729362  
  6         258  
15 6     6   55 use Moose::Util;
  6         8  
  6         32  
16 6     6   6643 use List::MoreUtils qw(uniq);
  0            
  0            
17             use Bread::Board::Container;
18              
19             our @EXPORT_OK = qw(load_container);
20              
21             # legacy code
22             sub new {
23             require Bread::Board::LazyLoader::Obj;
24             shift();
25             Bread::Board::LazyLoader::Obj->new(@_);
26             }
27              
28             my $load_container_params = Type::Params::compile(
29             slurpy Dict [
30             root_dir => Str | ArrayRef [Str],
31             filename_extension => Str,
32             container_name => Optional [Str],
33             container_factory => Optional [CodeRef],
34             ]
35             );
36              
37             sub load_container {
38             my ($params) = $load_container_params->(@_);
39              
40             my @root_dirs = map { is_ArrayRef($_) ? @$_ : $_ } $params->{root_dir};
41             my $filename_extension = $params->{filename_extension};
42             my $container_name = $params->{container_name} // 'Root';
43             my $container_factory = $params->{container_factory} // sub {
44             my ($name) = @_;
45             return Bread::Board::Container->new( name => $name );
46             };
47              
48             my $file_suffix = '.' . $filename_extension;
49             my $node
50             = _make_node( \@root_dirs, $file_suffix, $container_name,
51             $container_factory );
52             return _load_node($node);
53             }
54              
55             # role lazily load the sub_containers
56             sub _load_sub_container_role {
57             my ($children) = @_;
58              
59             my $role = Moose::Meta::Role->create_anon_role();
60             $role->add_around_method_modifier(
61             has_sub_container => sub {
62             my ( $orig, $this, $name ) = @_;
63              
64             return $this->$orig($name) || exists $children->{$name};
65             }
66             );
67             $role->add_around_method_modifier(
68             get_sub_container => sub {
69             my ( $orig, $this, $name ) = @_;
70              
71             my $sub_container = $this->$orig($name);
72             if ( !$sub_container && $children->{$name} ) {
73             $sub_container = _load_node( $children->{$name} );
74             $this->add_sub_container($sub_container);
75             }
76             return $sub_container;
77             }
78             );
79             $role->add_around_method_modifier(
80             get_sub_container_list => sub {
81             my $orig = shift;
82             return uniq( $orig->(@_), keys %$children );
83             }
84             );
85             return $role;
86             }
87              
88             # loads file in a sandbox package
89             my $Sandbox_num = 0;
90              
91             sub _load_file_content {
92             my ( $file ) = @_;
93              
94             my $package = $file;
95             $package =~ s/([^A-Za-z0-9_])/sprintf("_%2x", unpack("C", $1))/eg;
96              
97             my $sandbox_num = ++ $Sandbox_num;
98              
99             my $code = eval sprintf <<'END_EVAL', 'Bread::Board::LazyLoader', $sandbox_num, $package;
100             package %s::Sandbox::%d::%s;
101             {
102             my $code = do $file;
103             if ( !$code && ( my $error = $@ || $! )) { die $error; }
104             $code;
105             }
106             END_EVAL
107              
108             confess "Evaluation of '$file' failed with: $@" if $@;
109             ref($code) eq 'CODE'
110             or confess "Evaluation of file '$file' did not return a coderef";
111             return $code;
112             }
113              
114             sub _load_file {
115             my ( $name, $file, $next ) = @_;
116              
117             my $builder = _load_file_content($file);
118             is_CodeRef($builder)
119             or confess sprintf
120             "File '%s' returned wrong value, expected CodeRef, got '%s'",
121             $file, $builder;
122              
123             my $container = $builder->( $name, $next );
124             is_Object($container) && $container->isa('Bread::Board::Container')
125             or confess sprintf
126             "Container builder (coderef) from file '%s returned wrong value, expected Bread::Board::Container instance, got '%s'",
127             $file, $container;
128             $container->name eq $name
129             or confess sprintf
130             "Container builder (coderef) from file '%s returned container with wrong name, expected '%s', got '%s'",
131             $file, $name, $container->name;
132              
133             return $container;
134             }
135              
136             sub _load_files {
137             my ( $name, $files, $container_factory ) = @_;
138              
139             return @$files
140             ? do {
141             my ( $file, @rest ) = @$files;
142             _load_file(
143             $name, $file,
144             sub {
145             _load_files( $name, \@rest, $container_factory );
146             }
147             );
148             }
149             : $container_factory->($name);
150             }
151              
152             sub _load_node {
153             my ($node) = @_;
154              
155             my ( $name, $files, $children, $container_factory ) = @$node;
156              
157             my $container = _load_files( $name, $files, $container_factory );
158             Moose::Util::ensure_all_roles( $container,
159             _load_sub_container_role($children) );
160             return $container;
161             }
162              
163             sub _make_node {
164             my ( $dirs, $suffix, $root_name, $container_factory ) = @_;
165              
166             my $new = sub { [ shift(), [], {}, $container_factory ] };
167             my $add_to_parent = sub {
168             my ( $parent, $name ) = @_;
169             $parent->[2]{$name} //= $new->($name);
170             };
171             my $root = $new->($root_name);
172              
173             for my $dir (@$dirs) {
174              
175             # the only reason to pass coderef as third arg is that
176             # I do not want to create containers for empty dirs
177             dir($dir)->traverse(
178             sub {
179             my ( $f, $next, $level, $add ) = @_;
180              
181             if ( -d $f ) {
182             $next->(
183             $level + 1,
184             sub {
185             $add_to_parent->(
186             $add ? $add->( $f->basename ) : $root, shift()
187             );
188             }
189             );
190             }
191             elsif ( -f $f ) {
192             my ($name) = $f->basename =~ /(.*)$suffix$/ or return;
193              
194             my $node
195             = $level == 1 && $name eq $root_name
196             ? $root
197             : $add->($name);
198              
199             push @{ $node->[1] }, "$f";
200             }
201             },
202             0,
203             );
204             }
205              
206             return $root;
207             }
208              
209             1;
210              
211             # vim: expandtab:shiftwidth=4:tabstop=4:softtabstop=0:textwidth=78:
212              
213             __END__
214              
215             =pod
216              
217             =encoding UTF-8
218              
219             =head1 NAME
220              
221             Bread::Board::LazyLoader - loads lazily Bread::Board containers from files
222              
223             =head1 VERSION
224              
225             version 0.13
226              
227             =head1 SYNOPSIS
228              
229             use Bread::Board::LazyLoader qw(load_container);
230              
231             # having files defining Bread Board containers
232              
233             # ./ioc/Root.ioc
234             # ./ioc/Database.ioc
235             # ./ioc/Webapp/Rating.ioc
236              
237             # we can load them with
238              
239             my $root
240             = load_container( root_dir => './ioc', filename_extension => '.ioc', );
241              
242             # then $root container is defined by file Root.ioc
243             # $root->fetch('Database') is defined by file Database.ioc
244             # $root->fetch('Webapp/Rating.ioc') is defined by
245              
246             # but all files except of Root.ioc are loaded lazily when the respective
247             # container is needed (usually when a service from the container is
248             # resolved by a dependency)
249              
250             =head1 DESCRIPTION
251              
252             Bread::Board::LazyLoader loads a Bread::Board container from a directory
253             (directories) with files defining the container.
254              
255             The container returned can also loads lazily its sub containers from the same directories.
256              
257             =head1 FUNCTIONS
258              
259             All functions are imported on demand.
260              
261             =head2 load_container(%params)
262              
263             Loads the container. The parameters are:
264              
265             =over 4
266              
267             =item root_dir
268              
269             The directory (directories) to be traversed for container definition files.
270             Either string or an arrayref of strings. Mandatory parameter.
271              
272             =item filename_extension
273              
274             The extension of files (without dot) which are searched for container definitions.
275             Mandatory parameter.
276              
277             =item container_name
278              
279             The name of created container. Also the basename of the file which contains it.
280             "Root" by default.
281              
282             =item container_factory
283              
284             An anonymous subroutine used to create "intermediate" containers for directories - the ones
285             having no definition files. By default it is:
286              
287             sub {
288             my ($name) = @_;
289             return Bread::Board::Container->new(name => $name);
290             }
291              
292             =back
293              
294             C<< load_container >> searches under supplied root directories for plain files
295             with the extension. Found files found are used to build root container or its subcontainers.
296             The position of container in the hierarchy of the containers is same as the
297             relative path of the file (minus extension) under root directory.
298              
299             The exception is the file C<< Root >>.extension which defines the root container
300             itself, not its subcontainer called C<< Root >>.
301              
302             The container is built from its first definition file (the files are ordered
303             according their appropriate root in root_dir parameter).
304              
305             Definition file for a container is a perl code file returning (its last expression is)
306             an anonymous subroutine - a container builder.
307              
308             The container builder is called like:
309              
310             my $container = $builder->($name, $next);
311              
312             First argument to builder is a container name (the basename of the file found),
313             the second an anonymous subroutine creating the container via next definition file (if any)
314             or by calling the container factory.
315              
316             The definition file may look like:
317              
318             use strict;
319             use Bread::Board;
320              
321             sub {
322             my $name = shift;
323             return container $name => as {
324             service psgi => (...);
325             }
326             };
327              
328             Wwhen there is more than one root directory, the most specific should be
329             mentioned first and their would look like:
330              
331             use strict;
332             use Bread::Board;
333              
334             sub {
335             my ($name, $next) = @_;
336              
337             my $c = $next->();
338             return container $c => as {
339             # modifying container specified by more generic files
340             service psgi => (...);
341             }
342             };
343              
344             The builder must return a Bread::Board container (an instance of Bread::Board::Container or its subclass)
345             with name C<< $name >>.
346              
347             Every file is evaluated in a "sandbox", i.e. artificially created package,
348             thus all imports and sub definitions in the file are private and not shared.
349              
350             The root container is built immediately, the subcontainers (their files)
351             are built lazily, typically when a service from them is needed.
352              
353             =head1 AUTHOR
354              
355             Roman Daniel <roman@daniel.cz>
356              
357             =head1 COPYRIGHT AND LICENSE
358              
359             This software is copyright (c) 2016 by Roman Daniel.
360              
361             This is free software; you can redistribute it and/or modify it under
362             the same terms as the Perl 5 programming language system itself.
363              
364             =cut