blib/lib/Template/Parser/RemoteInclude.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 13 | 15 | 86.6 |
branch | n/a | ||
condition | n/a | ||
subroutine | 5 | 5 | 100.0 |
pod | n/a | ||
total | 18 | 20 | 90.0 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Template::Parser::RemoteInclude; | ||||||
2 | |||||||
3 | 1 | 1 | 63067 | use strict; | |||
1 | 2 | ||||||
1 | 37 | ||||||
4 | 1 | 1 | 7 | use warnings; | |||
1 | 2 | ||||||
1 | 46 | ||||||
5 | |||||||
6 | our $VERSION = '0.01'; | ||||||
7 | |||||||
8 | 1 | 1 | 889 | use namespace::autoclean; | |||
1 | 23406 | ||||||
1 | 7 | ||||||
9 | 1 | 1 | 1666 | use AnyEvent; | |||
1 | 6529 | ||||||
1 | 38 | ||||||
10 | 1 | 1 | 444 | use AnyEvent::Curl::Multi; | |||
0 | |||||||
0 | |||||||
11 | use HTTP::Request; | ||||||
12 | use Scalar::Util qw(refaddr); | ||||||
13 | use Try::Tiny; | ||||||
14 | use Sub::Install; | ||||||
15 | |||||||
16 | use base 'Template::Parser'; | ||||||
17 | |||||||
18 | # парсер ничего не знает о переменных, которые будут проинициализированы в шаблоне, т.к. он его ещё разбирает, | ||||||
19 | # но, вот получить хэш переменных - вполне необходимо, дабы упростить использование модуля | ||||||
20 | $Template::Parser::RemoteInclude::old_tt_process = \&Template::process; | ||||||
21 | Sub::Install::reinstall_sub({ | ||||||
22 | code => sub { | ||||||
23 | my $providers = $_[0]->service->context->{ LOAD_TEMPLATES }; | ||||||
24 | for (@$providers) { | ||||||
25 | next unless $_; | ||||||
26 | if (UNIVERSAL::isa($_->{PARSER},'Template::Parser::RemoteInclude')) { | ||||||
27 | $_->{PARSER}->{__stash} = $_[2] || {}; | ||||||
28 | last; | ||||||
29 | }; | ||||||
30 | } | ||||||
31 | return $Template::Parser::RemoteInclude::old_tt_process->(@_); | ||||||
32 | }, | ||||||
33 | into => "Template", | ||||||
34 | as => "process", | ||||||
35 | }); | ||||||
36 | |||||||
37 | |||||||
38 | =head1 NAME | ||||||
39 | |||||||
40 | Template::Parser::RemoteInclude - call remote template-server inside your template | ||||||
41 | |||||||
42 | =head1 DESCRIPTION | ||||||
43 | |||||||
44 | You can write your own html aggregator for block build pages. | ||||||
45 | However, this module allows you to make remote calls directly from the template. | ||||||
46 | This is very useful when your project have a template server. | ||||||
47 | |||||||
48 | This module allows you to make any http-requests from template. | ||||||
49 | |||||||
50 | Depends on L |
||||||
51 | |||||||
52 | L |
||||||
53 | |||||||
54 | Use and enjoy! | ||||||
55 | |||||||
56 | =head1 NOTE | ||||||
57 | |||||||
58 | =over 4 | ||||||
59 | |||||||
60 | =item * | ||||||
61 | |||||||
62 | Directive C |
||||||
63 | |||||||
64 | =item * | ||||||
65 | |||||||
66 | Parser does not know anything about L |
||||||
67 | |||||||
68 | =item * | ||||||
69 | |||||||
70 | Content of the response can be as a simple html or a new template | ||||||
71 | |||||||
72 | =item * | ||||||
73 | |||||||
74 | Contents of the response is recursively scanned for directives C |
||||||
75 | |||||||
76 | =item * | ||||||
77 | |||||||
78 | The best option when your template-server is located on the localhost | ||||||
79 | |||||||
80 | =back | ||||||
81 | |||||||
82 | =head1 SYNOPSIS | ||||||
83 | |||||||
84 | create C object with C |
||||||
85 | |||||||
86 | use Template; | ||||||
87 | use Template::Parser::RemoteInclude; | ||||||
88 | |||||||
89 | my $tt = Template->new( | ||||||
90 | INCLUDE_PATH => '....', | ||||||
91 | ...., | ||||||
92 | PARSER => Template::Parser::RemoteInclude->new( | ||||||
93 | 'Template::Parser' => { | ||||||
94 | ...., | ||||||
95 | }, | ||||||
96 | 'AnyEvent::Curl::Multi' => { | ||||||
97 | max_concurrency => 10, | ||||||
98 | ...., | ||||||
99 | } | ||||||
100 | ) | ||||||
101 | ); | ||||||
102 | |||||||
103 | simple example include content C |
||||||
104 | |||||||
105 | # example 1 | ||||||
106 | my $tmpl = "[% RINCLUDE GET 'http://ya.ru/' %]"; | ||||||
107 | $tt->process(\$tmpl,{}); | ||||||
108 | |||||||
109 | # example 2 - use variables passed in Template::process | ||||||
110 | my $tmpl = "[% RINCLUDE GET ya_url %]"; | ||||||
111 | $tt->process(\$tmpl,{ya_url => 'http://ya.ru/'}); | ||||||
112 | |||||||
113 | # example 3 - set headers | ||||||
114 | my $tmpl = "[% RINCLUDE GET ya_url ['header1' => 'value1','header2' => 'value2'] %]"; | ||||||
115 | $tt->process(\$tmpl,{ya_url => 'http://ya.ru/'}); | ||||||
116 | |||||||
117 | # example 4 - set headers | ||||||
118 | my $tmpl = "[% RINCLUDE GET ya_url headers %]"; | ||||||
119 | $tt->process(\$tmpl,{ya_url => 'http://ya.ru/', headers => ['header1' => 'value1','header2' => 'value2']}); | ||||||
120 | |||||||
121 | # example 5 - use HTTP::Request (with POST as http method) passed in Template::process | ||||||
122 | my $tmpl = "[% RINCLUDE http_req_1 %]"; | ||||||
123 | $tt->process( | ||||||
124 | \$tmpl, | ||||||
125 | { | ||||||
126 | http_req_1 => HTTP::Request->new( | ||||||
127 | POST => 'http://ya.ru/', | ||||||
128 | ['header1' => 'value1','header2' => 'value2'], | ||||||
129 | $content | ||||||
130 | ) | ||||||
131 | } | ||||||
132 | ); | ||||||
133 | |||||||
134 | example include remote template | ||||||
135 | |||||||
136 | # http://example.com/get/template/hello_world => | ||||||
137 | # 'Hello, [% name %]! [% name = "Boris" %][% RINCLUDE "http://example.com/.../another" %]' |
||||||
138 | # and | ||||||
139 | # http://example.com/.../another => | ||||||
140 | # 'And goodbye, [% name %]!' | ||||||
141 | |||||||
142 | # example | ||||||
143 | my $tmpl = "[% RINCLUDE GET 'http://example.com/get/template/hello_world' %]"; | ||||||
144 | $tt->process(\$tmpl,{name => 'User'}); | ||||||
145 | |||||||
146 | # returned | ||||||
147 | Hello, User! And goodbye, Boris! |
||||||
148 | |||||||
149 | more power example | ||||||
150 | |||||||
151 | use Template; | ||||||
152 | use Template::Parser::RemoteInclude; | ||||||
153 | |||||||
154 | my $tt = Template->new( | ||||||
155 | INCLUDE_PATH => '....', | ||||||
156 | ...., | ||||||
157 | PARSER => Template::Parser::RemoteInclude->new( | ||||||
158 | 'Template::Parser' => { | ||||||
159 | ...., | ||||||
160 | }, | ||||||
161 | 'AnyEvent::Curl::Multi' => { | ||||||
162 | max_concurrency => 10, | ||||||
163 | ...., | ||||||
164 | } | ||||||
165 | ), | ||||||
166 | WRAPPER => 'dummy.tt2' | ||||||
167 | ); | ||||||
168 | |||||||
169 | # where 'dummy.tt2' | ||||||
170 | # [% IF CSS %] | ||||||
171 | # [% FOREACH c = CSS %] | ||||||
172 | # css = [% c %] | ||||||
173 | # [% END %] | ||||||
174 | # [% END %] | ||||||
175 | # ==== | ||||||
176 | # [% content %] | ||||||
177 | # ==== | ||||||
178 | |||||||
179 | # http://example.com/get/template/hello_world => | ||||||
180 | # "[% CSS.push('http://example.com/file.css') %]\nHello, [% name %]!\n" | ||||||
181 | |||||||
182 | my $tmpl = "[% SET CSS = [] %][% RINCLUDE GET 'http://example.com/get/template/hello_world' %]"; | ||||||
183 | $tt->process(\$tmpl,{name => 'User'}); | ||||||
184 | |||||||
185 | # output: | ||||||
186 | # css = http://example.com/file.css | ||||||
187 | # | ||||||
188 | # ==== | ||||||
189 | # | ||||||
190 | # Hello, User! | ||||||
191 | # | ||||||
192 | # ==== | ||||||
193 | |||||||
194 | =head1 METHODS | ||||||
195 | |||||||
196 | =head2 new('Template::Parser' => %param1, 'AnyEvent::Curl::Multi' => %param2) | ||||||
197 | |||||||
198 | Simple constructor | ||||||
199 | |||||||
200 | =cut | ||||||
201 | sub new { | ||||||
202 | my ($class, %param) = @_; | ||||||
203 | |||||||
204 | my $self = $class->SUPER::new($param{'Template::Parser'}); | ||||||
205 | $self->{iparam} = $param{'AnyEvent::Curl::Multi'} || {}; | ||||||
206 | |||||||
207 | return $self; | ||||||
208 | } | ||||||
209 | |||||||
210 | sub _parse { | ||||||
211 | my ($self, $tokens, $info) = @_; | ||||||
212 | $self->{ _ERROR } = ''; | ||||||
213 | |||||||
214 | $self->{aecm} = AnyEvent::Curl::Multi->new(%{$self->{iparam}}); | ||||||
215 | |||||||
216 | # выгребем все id элементов массива с RINCLUDE и url в качесвте первого аргумента | ||||||
217 | my @ids_rinclude = (); | ||||||
218 | for (0..$#$tokens) { | ||||||
219 | if ( | ||||||
220 | UNIVERSAL::isa($tokens->[$_],'ARRAY') and | ||||||
221 | UNIVERSAL::isa($tokens->[$_]->[2],'ARRAY') and | ||||||
222 | $tokens->[$_]->[2]->[1] and | ||||||
223 | not ref $tokens->[$_]->[2]->[1] and | ||||||
224 | $tokens->[$_]->[2]->[1] eq 'RINCLUDE' | ||||||
225 | ) { | ||||||
226 | push @ids_rinclude, $_; | ||||||
227 | } | ||||||
228 | } | ||||||
229 | |||||||
230 | # хэш-связка: id элемента в массиве -> ссылка в памяти | ||||||
231 | my $ids_rinclude = {}; | ||||||
232 | # наполним хэш: ссылка в памяти -> объект запроса | ||||||
233 | my %requests = map { | ||||||
234 | my $req = $self->_make_request($tokens->[$_]); | ||||||
235 | return unless $req; | ||||||
236 | my $addr = refaddr($req); | ||||||
237 | $ids_rinclude->{$_} = $addr; | ||||||
238 | ($addr => $req); | ||||||
239 | } @ids_rinclude; | ||||||
240 | |||||||
241 | # зарегистрируем запросы в Curl::Multi | ||||||
242 | my @handler_cm = map {$self->{aecm}->request($_)} values %requests; | ||||||
243 | |||||||
244 | # колбэчимся и в колбэке переопределяем значения в %requests | ||||||
245 | $self->{aecm}->reg_cb(response => sub { | ||||||
246 | my ($client, $request, $response, $stats) = @_; | ||||||
247 | $requests{refaddr($request)} = $response->content; | ||||||
248 | #$requests{refaddr($request)} = "[% CSS.push('http://example.com/file.css') %]\nHello, [% name %]!\n"; | ||||||
249 | }); | ||||||
250 | |||||||
251 | $self->{aecm}->reg_cb(error => sub { | ||||||
252 | my ($client, $request, $errmsg, $stats) = @_; | ||||||
253 | $self->debug("error returned RINCLUDE for url: ".$request->uri." - $errmsg") if $self->{ DEBUG }; | ||||||
254 | $self->error("RINCLUDE for url: ".$request->uri." - $errmsg"); | ||||||
255 | #$requests{refaddr($request)} = $errmsg; | ||||||
256 | }); | ||||||
257 | |||||||
258 | # поднимаем событие обхода для Curl::Multi | ||||||
259 | $self->{timer_w} = AE::timer(0, 0, sub { $self->{aecm}->_perform }) if (@handler_cm and not $self->{timer_w}); | ||||||
260 | |||||||
261 | # погнали (see AnyEvent::Curl::Multi) | ||||||
262 | for my $crawler (@handler_cm) { | ||||||
263 | try { | ||||||
264 | $crawler->cv->recv; | ||||||
265 | } catch { | ||||||
266 | $self->debug("error returned RINCLUDE for url: ".$crawler->{req}->uri." - $_") if $self->{ DEBUG }; | ||||||
267 | $self->error("RINCLUDE for url: ".$crawler->{req}->uri." - $_"); | ||||||
268 | #$requests{refaddr($crawler->{req})} = $_; | ||||||
269 | }; | ||||||
270 | }; | ||||||
271 | |||||||
272 | return if $self->error; | ||||||
273 | |||||||
274 | # # replace tokens RINCLUDE to simple value | ||||||
275 | # for (@ids_rinclude) { | ||||||
276 | # $tokens->[$_] = [ | ||||||
277 | # "'".$ids_rinclude->{$_}."'", # unic name - addr | ||||||
278 | # 1, | ||||||
279 | # $self->split_text($requests{$ids_rinclude->{$_}}) | ||||||
280 | # ]; | ||||||
281 | # } | ||||||
282 | |||||||
283 | # extend tokens RINCLUDE to new array values from request | ||||||
284 | for (@ids_rinclude) { | ||||||
285 | my $parse_upload = $self->split_text($requests{$ids_rinclude->{$_}}); | ||||||
286 | my $added_len = $#$parse_upload; | ||||||
287 | splice(@$tokens, $_, 1, @$parse_upload); | ||||||
288 | $_ += $added_len for @ids_rinclude; | ||||||
289 | } | ||||||
290 | |||||||
291 | my $cli = delete $self->{aecm}; | ||||||
292 | undef $cli; | ||||||
293 | |||||||
294 | # методично, как тузик тряпку, продожаем обработку токенов, пока не исчерпаем все RINCLUDE, если они пришли в контенте ответов | ||||||
295 | if (@ids_rinclude) { | ||||||
296 | return $self->_parse($tokens, $info); | ||||||
297 | } else { | ||||||
298 | delete $self->{__stash}; | ||||||
299 | return $self->SUPER::_parse($tokens, $info); | ||||||
300 | } | ||||||
301 | } | ||||||
302 | |||||||
303 | sub _strip { | ||||||
304 | my $text = shift; | ||||||
305 | return $text unless $text; | ||||||
306 | $text =~ s/(^$1|$1$)//g if $text =~ /^(['"])/; | ||||||
307 | return $text; | ||||||
308 | } | ||||||
309 | |||||||
310 | sub _make_request { | ||||||
311 | my $self = shift; | ||||||
312 | my $token = shift; | ||||||
313 | return unless (UNIVERSAL::isa($token,'ARRAY') and UNIVERSAL::isa($token->[2],'ARRAY')); | ||||||
314 | my @token = @{$token->[2]}; | ||||||
315 | |||||||
316 | # skip RINCLUDE | ||||||
317 | splice(@token,0,2); | ||||||
318 | |||||||
319 | my $ret = HTTP::Request->new(); | ||||||
320 | my $is_header = 0; my @headers = (); | ||||||
321 | while (@token) { | ||||||
322 | my $type = shift @token || ''; | ||||||
323 | my $val = shift @token || ''; | ||||||
324 | $val = _strip($val) || ''; | ||||||
325 | |||||||
326 | next if ($type eq 'ASSIGN' or $type eq 'COMMA'); | ||||||
327 | |||||||
328 | if ($type eq 'IDENT') { | ||||||
329 | $type = 'LITERAL'; | ||||||
330 | $val = $self->{__stash}->{$val}; | ||||||
331 | if (UNIVERSAL::isa($val, 'HTTP::Request')) { | ||||||
332 | $self->debug("uri found: ".$val->uri) if $self->{ DEBUG }; | ||||||
333 | return $val; | ||||||
334 | }; | ||||||
335 | }; | ||||||
336 | |||||||
337 | if ($val eq 'GET' or $val eq 'POST' or $val eq 'PUT' or $val eq 'DELETE') { | ||||||
338 | $ret->method($val); | ||||||
339 | } elsif ($type eq '[') { | ||||||
340 | $is_header = 1; | ||||||
341 | } elsif ($type eq ']') { | ||||||
342 | $is_header = 0; | ||||||
343 | while (@headers) {$ret->header(splice(@headers,0,2))} | ||||||
344 | } elsif ($type eq 'LITERAL' and $is_header) { | ||||||
345 | push @headers, UNIVERSAL::isa($val, 'ARRAY') ? @$val : $val; | ||||||
346 | } elsif ($type eq 'LITERAL' and not $is_header) { | ||||||
347 | if ($ret->uri) { | ||||||
348 | $ret->content($val); | ||||||
349 | } else { | ||||||
350 | $ret->uri($val); | ||||||
351 | $self->debug("uri found: ".$ret->uri) if $self->{ DEBUG }; | ||||||
352 | }; | ||||||
353 | } else { | ||||||
354 | # skip unknown | ||||||
355 | next; | ||||||
356 | } | ||||||
357 | } | ||||||
358 | |||||||
359 | return $ret; | ||||||
360 | } | ||||||
361 | |||||||
362 | =head1 SEE ALSO | ||||||
363 | |||||||
364 | L |
||||||
365 | |||||||
366 | =head1 AUTHOR | ||||||
367 | |||||||
368 | mr.Rico |
||||||
369 | |||||||
370 | =cut | ||||||
371 | |||||||
372 | 1; | ||||||
373 | __END__ |