File Coverage

blib/lib/Kelp/Module/Symbiosis.pm
Criterion Covered Total %
statement 53 54 98.1
branch 15 22 68.1
condition 8 14 57.1
subroutine 10 10 100.0
pod 4 4 100.0
total 90 104 86.5


line stmt bran cond sub pod time code
1             package Kelp::Module::Symbiosis;
2              
3             our $VERSION = '1.10';
4              
5 6     6   185784 use Kelp::Base qw(Kelp::Module);
  6         20340  
  6         43  
6 6     6   7547 use Plack::App::URLMap;
  6         25532  
  6         178  
7 6     6   40 use Carp;
  6         18  
  6         369  
8 6     6   36 use Scalar::Util qw(blessed refaddr);
  6         10  
  6         5252  
9              
10             attr "-mounted" => sub { {} };
11             attr "-loaded" => sub { {} };
12              
13             sub mount
14             {
15 14     14 1 804 my ($self, $path, $app) = @_;
16 14         32 my $mounted = $self->mounted;
17              
18 14 100 66     89 if (!ref $app && $app) {
19 2         5 my $loaded = $self->loaded;
20             croak "Symbiosis: cannot mount $app, because no such name was loaded"
21 2 50       12 unless $loaded->{$app};
22 2         5 $app = $loaded->{$app};
23             }
24              
25             carp "Symbiosis: overriding mounting point $path"
26 14 50       35 if exists $mounted->{$path};
27 14         48 $mounted->{$path} = $app;
28 14         22 return scalar keys %{$mounted};
  14         47  
29             }
30              
31             sub _link
32             {
33 7     7   71 my ($self, $name, $app, $mount) = @_;
34 7         24 my $loaded = $self->loaded;
35              
36             warn "Symbiosis: overriding module name $name"
37 7 50       35 if exists $loaded->{$name};
38 7         15 $loaded->{$name} = $app;
39              
40 7 100       16 if ($mount) {
41 1         6 $self->mount($mount, $app);
42             }
43 7         12 return scalar keys %{$loaded};
  7         23  
44             }
45              
46             sub run_all
47             {
48 1     1 1 801 my ($self) = shift;
49              
50 1         42 warn 'Symbiosis: run_all method is deprecated, use run instead';
51 1         9 return $self->run(@_);
52             }
53              
54             sub run
55             {
56 19     19 1 91 my ($self) = shift;
57 19         134 my $psgi_apps = Plack::App::URLMap->new;
58 19         229 my %addrs; # apps keyed by refaddr
59              
60 19         35 my $error = "Symbiosis: cannot start the ecosystem because";
61 19         39 while (my ($path, $app) = each %{$self->mounted}) {
  67         1559  
62 48 100       463 if (blessed $app) {
    50          
63 45 50       205 croak "$error application mounted under $path cannot run()"
64             unless $app->can("run");
65              
66             # cache the ran application so that it won't be ran twice
67 45         110 my $addr = refaddr $app;
68 45   66     228 my $ran = $addrs{$addr} //= $app->run(@_);
69              
70 45         986 $psgi_apps->map($path, $ran);
71             }
72             elsif (ref $app eq 'CODE') {
73 3         8 $psgi_apps->map($path, $app);
74             }
75             else {
76 0         0 croak "$error mount point $path is neither an object nor a coderef";
77             }
78             }
79              
80 19         190 return $psgi_apps->to_app;
81             }
82              
83             sub build
84             {
85 5     5 1 467 my ($self, %args) = @_;
86             $args{mount} //= '/'
87 5 100 50     28 unless exists $args{mount};
88              
89             warn 'Symbiosis: automount configuration is deprecated, use mount instead'
90 5 50       17 if exists $args{automount};
91              
92 5 50 33     24 if ($args{mount} && (!exists $args{automount} || $args{automount})) {
      66        
93 2         6 $self->mount($args{mount}, $self->app);
94             }
95              
96             $self->register(
97             symbiosis => $self,
98 18     18   182 run_all => sub { shift->symbiosis->run(@_); },
99 5         39 );
100              
101             }
102              
103             1;
104             __END__
105              
106             =head1 NAME
107              
108             Kelp::Module::Symbiosis - Manage an entire ecosystem of Plack organisms under Kelp
109              
110             =head1 SYNOPSIS
111              
112             # in configuration file
113             modules => [qw/Symbiosis SomeSymbioticModule/],
114             modules_init => {
115             Symbiosis => {
116             mount => '/kelp', # a path to mount Kelp main instance
117             },
118             SomeSymbioticModule => {
119             mount => '/elsewhere', # a path to mount SomeSymbioticModule
120             },
121             },
122              
123             # in kelp application - can be skipped if all mount paths are specified in config above
124             my $symbiosis = $kelp->symbiosis;
125             $symbiosis->mount('/app-path' => $kelp);
126             $symbiosis->mount('/other-path' => $kelp->module_method);
127             $symbiosis->mount('/other-path' => 'module_name'); # alternative - finds a module by name
128              
129             # in psgi script
130             my $app = KelpApp->new();
131             $app->run_all; # instead of run
132              
133             =head1 DESCRIPTION
134              
135             This module is an attempt to standardize the way many standalone Plack applications should be ran alongside the Kelp framework. The intended use is to introduce new "organisms" into symbiotic interaction by creating Kelp modules that are then attached onto Kelp. Then, the added method I<run_all> should be invoked in place of Kelp's I<run>, which will construct a L<Plack::App::URLMap> and return it as an application.
136              
137             =head2 Why not just use Plack::Builder in a .psgi script?
138              
139             One reason is not to put too much logic into .psgi script. It my opinion a framework should be capable enough not to make adding an additional application feel like a hack. This is of course subjective.
140              
141             The main functional reason to use this module is the ability to access the Kelp application instance inside another Plack application. If that application is configurable, it can be configured to call Kelp methods. This way, Kelp can become a glue for multiple standalone Plack applications, the central point of a Plack mixture:
142              
143             # in Symbiont's Kelp module (extends Kelp::Module::Symbiosis::Base)
144              
145             sub psgi {
146             my ($self) = @_;
147              
148             my $app = Some::Plack::App->new(
149             on_something => sub {
150             my $kelp = $self->app; # we can access Kelp!
151             $kelp->something_happened;
152             },
153             );
154              
155             return $app->to_app;
156             }
157              
158             # in Kelp application class
159              
160             sub something_happened {
161             ... # handle another app's signal
162             }
163              
164             =head2 What can be mounted?
165              
166             The sole requirement for a module to be mounted into Symbiosis is its ability to I<run()>, returning the psgi application. A module also needs to be a blessed reference, of course. Fun fact: Symbiosis module itself meets that requirements, so one symbiotic app can be mounted inside another.
167              
168             It can also be just a plain psgi app, which happens to be a code reference.
169              
170             Whichever it is, it should be a psgi application ready to be ran by the server, wrapped in all the needed middlewares. This is made easier with L<Kelp::Module::Symbiosis::Base>, which allows you to add symbionts in the configuration for Kelp along with the middlewares. Because of this, this should be a preferred way of defining symbionts.
171              
172             For very simple use cases, this will work though:
173              
174             # in application build method
175             my $some_app = SomePlackApp->new->to_app;
176             $self->symbiosis->mount('/path', $some_app);
177              
178             =head1 METHODS
179              
180             =head2 mount
181              
182             sig: mount($self, $path, $app)
183              
184             Adds a new $app to the ecosystem under $path. I<$app> can be:
185              
186             =over
187              
188             =item
189              
190             A blessed reference - will try to call run on it
191              
192             =item
193              
194             A code reference - will try calling it
195              
196             =item
197              
198             A string - will try finding a symbiotic module with that name and mounting it. See L<Kelp::Module::Symbiosis::Base/name>
199              
200             =back
201              
202             =head2 run_all
203              
204             sig: run_all($self)
205              
206             I<DEPRECATED in 1.01 use L</run> instead. ETA on removal is no less than three months>
207              
208             =head2 run
209              
210             Constructs and returns a new L<Plack::App::URLMap> with all the mounted modules and Kelp itself.
211              
212             Note: it will not run mounted object twice. This means that it is safe to mount something in two paths at once, and it will just be an alias to the same application.
213              
214             =head2 mounted
215              
216             sig: mounted($self)
217              
218             Returns a hashref containing a list of mounted modules, keyed by their specified mount paths.
219              
220             =head2 loaded
221              
222             sig: loaded($self)
223              
224             I<new in 1.10>
225              
226             Returns a hashref containing a list of loaded modules, keyed by their names.
227              
228             A module is loaded once it is added to Kelp configuration. This can be used to access a module that does not introduce new methods to Kelp.
229              
230             =head1 METHODS INTRODUCED TO KELP
231              
232             =head2 symbiosis
233              
234             Returns an instance of this class.
235              
236             =head2 run_all
237              
238             Shortcut method, same as C<< $kelp->symbiosis->run() >>.
239              
240             =head1 CONFIGURATION
241              
242             # Symbiosis MUST be specified as the first one
243             modules => [qw/Symbiosis Module::Some/],
244             modules_init => {
245             Symbiosis => {
246             mount => '/kelp',
247             },
248             'Module::Some' => {
249             mount => '/some',
250             ...
251             },
252             }
253              
254             Symbiosis should be the first of the symbiotic modules specified in your Kelp configuration. Failure to meet this requirement will cause your application to crash immediately.
255              
256             =head2 automount
257              
258             I<DEPRECATED in 1.10: use 'mount' instead. ETA on removal is no less than three months>
259              
260             Whether to automatically call I<mount> for the Kelp instance, which will be mounted to root path I</>. Defaults to I<1>.
261              
262             If you set this to I<0> you will have to run something like C<< $kelp->symbiosis->mount($mount_path, $kelp); >> in Kelp's I<build> method. This will allow other paths than root path for the base Kelp application, if needed.
263              
264             =head2 mount
265              
266             I<new in 1.10>
267              
268             A path to mount the Kelp instance, which defaults to I<'/'>. Specify a string if you wish a to use different path. Specify an I<undef> or empty string to avoid mounting at all - you will have to run something like C<< $kelp->symbiosis->mount($mount_path, $kelp); >> in Kelp's I<build> method.
269              
270             Collides with now deprecated L</automount> - if you specify both, automount will control if the app will be mounted where the I<mount> points to.
271              
272             =head1 CAVEATS
273              
274             Routes specified in symbiosis will be matched before routes in Kelp. Once you mount something under I</api> for example, you will no longer be able to specify Kelp route for anything under I</api>.
275              
276             =head1 SEE ALSO
277              
278             =over 2
279              
280             =item * L<Kelp::Module::Symbiosis::Base>, a base for symbiotic modules
281              
282             =item * L<Kelp::Module::WebSocket::AnyEvent>, a reference symbiotic module
283              
284             =item * L<Plack::App::URLMap>, Plack URL mapper application
285              
286             =back
287              
288             =head1 AUTHOR
289              
290             Bartosz Jarzyna, E<lt>brtastic.dev@gmail.comE<gt>
291              
292             =head1 COPYRIGHT AND LICENSE
293              
294             Copyright (C) 2020 by Bartosz Jarzyna
295              
296             This library is free software; you can redistribute it and/or modify
297             it under the same terms as Perl itself, either Perl version 5.10.0 or,
298             at your option, any later version of Perl 5 you may have available.
299              
300              
301             =cut