File Coverage

blib/lib/Kelp/Module/Symbiosis.pm
Criterion Covered Total %
statement 63 69 91.3
branch 17 26 65.3
condition 5 8 62.5
subroutine 13 15 86.6
pod 3 3 100.0
total 101 121 83.4


line stmt bran cond sub pod time code
1             package Kelp::Module::Symbiosis;
2              
3             our $VERSION = '1.12';
4              
5 6     6   104382 use Kelp::Base qw(Kelp::Module);
  6         16  
  6         40  
6 6     6   4297 use Plack::App::URLMap;
  6         11843  
  6         167  
7 6     6   39 use Carp;
  6         14  
  6         374  
8 6     6   37 use Scalar::Util qw(blessed refaddr);
  6         12  
  6         317  
9 6     6   3023 use Plack::Middleware::Conditional;
  6         9583  
  6         188  
10 6     6   38 use Plack::Util;
  6         16  
  6         113  
11 6     6   2909 use Kelp::Module::Symbiosis::_Util;
  6         14  
  6         5874  
12              
13             attr -mounted => sub { {} };
14             attr -loaded => sub { {} };
15             attr -middleware => sub { [] };
16             attr reverse_proxy => 0;
17              
18             sub mount
19             {
20 16     16 1 837 my ($self, $path, $app) = @_;
21 16         39 my $mounted = $self->mounted;
22              
23 16 100 66     95 if (!ref $app && $app) {
24 2         5 my $loaded = $self->loaded;
25             croak "Symbiosis: cannot mount $app, because no such name was loaded"
26 2 50       12 unless $loaded->{$app};
27 2         4 $app = $loaded->{$app};
28             }
29              
30             carp "Symbiosis: overriding mounting point $path"
31 16 50       44 if exists $mounted->{$path};
32 16         32 $mounted->{$path} = $app;
33 16         28 return scalar keys %{$mounted};
  16         41  
34             }
35              
36             sub _link
37             {
38 8     8   80 my ($self, $name, $app, $mount) = @_;
39 8         26 my $loaded = $self->loaded;
40              
41             warn "Symbiosis: overriding module name $name"
42 8 50       37 if exists $loaded->{$name};
43 8         19 $loaded->{$name} = $app;
44              
45 8 100       23 if ($mount) {
46 1         3 $self->mount($mount, $app);
47             }
48 8         14 return scalar keys %{$loaded};
  8         30  
49             }
50              
51             sub run
52             {
53 20     20 1 113 my ($self) = shift;
54 20         144 my $psgi_apps = Plack::App::URLMap->new;
55 20         244 my %addrs; # apps keyed by refaddr
56              
57 20         41 my $error = "Symbiosis: cannot start the ecosystem because";
58 20         34 while (my ($path, $app) = each %{$self->mounted}) {
  72         1751  
59 52 100       519 if (blessed $app) {
    50          
60 49 50       269 croak "$error application mounted under $path cannot run()"
61             unless $app->can("run");
62              
63             # cache the ran application so that it won't be ran twice
64 49         145 my $addr = refaddr $app;
65 49   66     264 my $ran = $addrs{$addr} //= $app->run(@_);
66              
67 49         1026 $psgi_apps->map($path, $ran);
68             }
69             elsif (ref $app eq 'CODE') {
70 3         10 $psgi_apps->map($path, $app);
71             }
72             else {
73 0         0 croak "$error mount point $path is neither an object nor a coderef";
74             }
75             }
76              
77 20         210 my $wrapped = Kelp::Module::Symbiosis::_Util::wrap($self, $psgi_apps->to_app);
78 20         68 return $self->_reverse_proxy_wrap($wrapped);
79             }
80              
81             sub _reverse_proxy_wrap
82             {
83 20     20   44 my ($self, $app) = @_;
84 20 50       56 return $app unless $self->reverse_proxy;
85              
86 0         0 my $mw_class = Plack::Util::load_class('ReverseProxy', 'Plack::Middleware');
87             return Plack::Middleware::Conditional->wrap(
88             $app,
89 0 0   0   0 condition => sub { !$_[0]{REMOTE_ADDR} || $_[0]{REMOTE_ADDR} =~ m{127\.0\.0\.1} },
90 0     0   0 builder => sub { $mw_class->wrap($_[0]) },
91 0         0 );
92             }
93              
94             sub build
95             {
96 6     6 1 587 my ($self, %args) = @_;
97             $args{mount} //= '/'
98 6 100 50     31 unless exists $args{mount};
99              
100 6 100       22 if ($args{mount}) {
101 2         7 $self->mount($args{mount}, $self->app);
102             }
103              
104 6 50       26 if ($args{reverse_proxy}) {
105 0         0 $self->reverse_proxy(1);
106             }
107              
108 6         27 Kelp::Module::Symbiosis::_Util::load_middleware($self, %args);
109              
110             $self->register(
111             symbiosis => $self,
112 20     20   198 run_all => sub { shift->symbiosis->run(@_); },
113 6         45 );
114              
115             }
116              
117             1;
118             __END__
119              
120             =head1 NAME
121              
122             Kelp::Module::Symbiosis - Manage an entire ecosystem of Plack organisms under Kelp
123              
124             =head1 SYNOPSIS
125              
126             # in configuration file
127             modules => [qw/Symbiosis SomeSymbioticModule/],
128             modules_init => {
129             Symbiosis => {
130             mount => '/kelp', # a path to mount Kelp main instance
131             },
132             SomeSymbioticModule => {
133             mount => '/elsewhere', # a path to mount SomeSymbioticModule
134             },
135             },
136              
137             # in kelp application - can be skipped if all mount paths are specified in config above
138             my $symbiosis = $kelp->symbiosis;
139             $symbiosis->mount('/app-path' => $kelp);
140             $symbiosis->mount('/other-path' => $kelp->module_method);
141             $symbiosis->mount('/other-path' => 'module_name'); # alternative - finds a module by name
142              
143             # in psgi script
144             my $app = KelpApp->new();
145             $app->run_all; # instead of run
146              
147             =head1 DESCRIPTION
148              
149             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.
150              
151             =head2 Why not just use Plack::Builder in a .psgi script?
152              
153             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.
154              
155             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:
156              
157             # in Symbiont's Kelp module (extends Kelp::Module::Symbiosis::Base)
158              
159             sub psgi {
160             my ($self) = @_;
161              
162             my $app = Some::Plack::App->new(
163             on_something => sub {
164             my $kelp = $self->app; # we can access Kelp!
165             $kelp->something_happened;
166             },
167             );
168              
169             return $app->to_app;
170             }
171              
172             # in Kelp application class
173              
174             sub something_happened {
175             ... # handle another app's signal
176             }
177              
178             =head2 What can be mounted?
179              
180             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.
181              
182             It can also be just a plain psgi app, which happens to be a code reference.
183              
184             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.
185              
186             For very simple use cases, this will work though:
187              
188             # in application build method
189             my $some_app = SomePlackApp->new->to_app;
190             $self->symbiosis->mount('/path', $some_app);
191              
192             =head1 METHODS
193              
194             =head2 mount
195              
196             sig: mount($self, $path, $app)
197              
198             Adds a new $app to the ecosystem under $path. I<$app> can be:
199              
200             =over
201              
202             =item
203              
204             A blessed reference - will try to call run on it
205              
206             =item
207              
208             A code reference - will try calling it
209              
210             =item
211              
212             A string - will try finding a symbiotic module with that name and mounting it. See L<Kelp::Module::Symbiosis::Base/name>
213              
214             =back
215              
216             =head2 run
217              
218             Constructs and returns a new L<Plack::App::URLMap> with all the mounted modules and Kelp itself.
219              
220             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.
221              
222             =head2 mounted
223              
224             sig: mounted($self)
225              
226             Returns a hashref containing a list of mounted modules, keyed by their specified mount paths.
227              
228             =head2 loaded
229              
230             sig: loaded($self)
231              
232             I<new in 1.10>
233              
234             Returns a hashref containing a list of loaded modules, keyed by their names.
235              
236             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.
237              
238             =head1 METHODS INTRODUCED TO KELP
239              
240             =head2 symbiosis
241              
242             Returns an instance of this class.
243              
244             =head2 run_all
245              
246             Shortcut method, same as C<< $kelp->symbiosis->run() >>.
247              
248             =head1 CONFIGURATION
249              
250             # Symbiosis MUST be specified as the first one
251             modules => [qw/Symbiosis Module::Some/],
252             modules_init => {
253             Symbiosis => {
254             mount => '/kelp',
255             },
256             'Module::Some' => {
257             mount => '/some',
258             ...
259             },
260             }
261              
262             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.
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             =head2 reverse_proxy
271              
272             I<new in 1.11>
273              
274             A boolean flag (I<1/0>) which enables reverse proxy for all the Plack apps at once. Requires L<Plack::Middleware::ReverseProxy> to be installed.
275              
276             =head2 middleware, middleware_init
277              
278             I<new in 1.12>
279              
280             Middleware specs for the entire ecosystem. Every application mounted in Symbiosis will be wrapped in these middleware. They are configured exactly the same as middlewares in Kelp. Regular Kelp middleware will be used just for the Kelp application, so if you want to wrap all symbionts at once, this is the place to do it.
281              
282             =head1 CAVEATS
283              
284             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>.
285              
286             =head1 SEE ALSO
287              
288             =over 2
289              
290             =item * L<Kelp::Module::Symbiosis::Base>, a base for symbiotic modules
291              
292             =item * L<Kelp::Module::WebSocket::AnyEvent>, a reference symbiotic module
293              
294             =item * L<Plack::App::URLMap>, Plack URL mapper application
295              
296             =back
297              
298             =head1 AUTHOR
299              
300             Bartosz Jarzyna, E<lt>brtastic.dev@gmail.comE<gt>
301              
302             =head1 COPYRIGHT AND LICENSE
303              
304             Copyright (C) 2020 by Bartosz Jarzyna
305              
306             This library is free software; you can redistribute it and/or modify
307             it under the same terms as Perl itself, either Perl version 5.10.0 or,
308             at your option, any later version of Perl 5 you may have available.
309              
310              
311             =cut