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