| 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__ |