lunes, 30 de noviembre de 2009

PSGI y Plack: el futuro de las aplicaciones web

[English translation]
Hace unas semanas le mostraba a mi amigo Joel un one-liner de Perl que implementaba un servidor web, tal vez tenía mucho trabajo que hacer porque no pareció sorprendido por esta fantástica línea de perl que usa el módulo IO::All:

perl -MIO::All -e 'io(":8080")->fork->accept->(sub { $_[0] < io(-x $1 ? "./$1 |" : $1) if /^GET \/(.*) / })'

Pero además sorprendentemente (sobre todo para un fanático de Perl) su respuesta fue: "Sabes que la gente de Python tiene software para implementar servidores web con muchísima facilidad, aunque no se cuál es", ahí me dí cuenta de que no entendió el punto, tal vez lo agarre en un mal momento así que lo deje ir.

Pero ya me había intrigado, aunque estaba seguro que se refería a WSGI (también conocido como el PEP-333): una especificación para un API de aplicaciones web, permitiendo la separación e responsabilidades entre la interfaz (política) y su implementación (mecanismos), de manera que los desarrolladores se pueden preocupar por desarrollar y optimizar los mecanismos independientemente de las aplicaciones que los utilizan.

En Perl ese era el trabajo de HTTP::Engine utilizado entre otros por Catalyst.

Sin embargo, me quedó la curiosidad y me puse a buscar en CPAN, ¿habría algo nuevo por allí?. Encontré módulos como Mojo, que utilizan internamente una interfaz similar a WSGI, sin embargo lo más interesante que conseguí fue PSGI y Plack.

Al parecer HTTP::Engine está lejos de ser una solución ideal. Según he leído es monolítico, difícil de adaptar y no muy eficiente, para ambientes integrados (embedded) supongo. Lo cierto es que Miyagawa decidió separar HTTP::Engine en tres partes:
  1. La Especificación: PSGI
  2. Una implementación de referencia: Plack::Server
  3. Herramientas: Plack::*

Lo más interesante de PSGI y Plack es la rapidez con la que se implementó, hace solo semanas era una idea y ya están disponibles desde hace algún tiempo implementaciones de referencia que permiten ejecutar aplicaciones Plack por si mismas (standalone) en un solo hilo o con perfork, también hay interfaces para FastCGI, CGI y por supuesto mod-perl, y como si esto fuera poco, PSGI tiene la capacidad trabajar con sin bloqueo de entrada/salida (non blocking I/O), así que se hicieron servidores basados en POE, AnyEvent y Coro, incluso ya está disponible un módulo de PSGI para Apache (mod-psgi).

Por otra parte, de la nada aparecieron adaptadores de PSGI para frameworks como Catalyst (Catalyst::Engine::PSGI), Squatting (Squatting::On::PSGI), CGI::Application (CGI::Application::PSGI), Dancer y hasta para WebGUI (PlebGUI), también hay herramientas para facilitar la migración de otras tecnologías a PSGI, por ejemplo si tienes alguna aplicación escrita para HTTP::Engine, puedes utilizarla prácticamente sin modificación en PSGI con HTTP::Engine::Interface::PSGI , si tienes alguna aplicación CGI tienes la oportunidad de migrarla con muy pocas modificaciones con CGI::PSGI, y si aún esto es demasiado trabajo puedes usar CGI::Emulate::PSGI que permite ejecutar los CGI como un servidor desde la línea de comandos!.

En el articulo anterior hice un servidorcito de documentos POD que implementé como CGI, seguramente más de uno tuvo problemas para hacerlo funcionar, porque hay que montar el web server y configurar el CGI entre otros. Usando CGI::Emulate::PSGI solamente escribimos un programa que inicie el servidor (perldocweb_starter):

1 use CGI::Emulate::PSGI;
2 my $app = CGI::Emulate::PSGI->handler(sub { do "perldocweb" })

y luego ejecutamos el comando plackup:

$ plackup perldocweb_starter
Plack::Server::Standalone: Accepting connections at http://0:5000/

y ahora tenemos nuestro servidor de documentación ejecutándose en el puerto 5000, así que al visitar:

http://localhost:5000/perldocweb?PSGI

Debería aparecer la especificación de PSGI en el navegador, fácil ¿no?.

Ahora si estamos dispuestos a tocar el código del programa, no necesitaremos el emulador y podremos ejecutar la aplicación directamente con plackup, lo cual es mucho más eficiente.

La primera modificación es cambiar la línea 4 para usar CGI::PSGI, además ya no se usa CGI::Carp, porque Plack tiene una manera mucho más elegante de mostrar los errores utilizando Devel::StackTrace::AsHTML.

Cuando usamos CGI::PSGI el programa debe crear (y retornar) una clausura que será nuestra aplicación así que el código principal entre las líneas 20 y 50 debe encerrarse en una clausura, además la línea 20 ahora debe inicializar un objeto CGI::PSGI, así que la reemplazamos por las líneas 20 a 22 de en la nueva aplicación:

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use CGI::PSGI;
 5 use IO::File;
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my %content_types = (
10     RTF   => "application/rtf",
11     LaTeX => "application/x-latex",
12     PDF   => "application/pdf",
13 );
14 my @wikis   = qw(Usemod Twiki Template Kwiki Confluence Moinmoin Tiddlywiki Mediawiki Textile);
15 my %formats = (
16     ( map { $_ => "Pod::Simple::$_" } keys %content_types ),
17     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
18 );
19 
20 my $app = sub {
21     my $env      = shift;
22     my $q        = CGI::PSGI->new($env);
23     my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
24     my $format   = $q->param("format") || "HTML";
25     given ($format) {
26         when ("source") {
27             return [ $q->psgi_header("text/plain"), IO::File->new($filename) ];
28         }
29         when ('HTML') {
30             my $parser = Pod::Simple::HTML->new;
31             $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
32             my $footer = "<hr>"
33                 . join( " ", map { make_link( $_, $q ) } "source", keys %content_types )
34                 . " | Wiki formats: "
35                 . join( " ", map { make_link( $_, $q ) } @wikis );
36             $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
37             $parser->output_string( my $output );
38             $parser->parse_file($filename);
39             return [ $q->psgi_header("text/html"), [$output] ];
40         }
41         when (%formats) {
42             my $class = $formats{$format};
43             eval "require $class";
44             my $parser = $class->new;
45             $parser->output_string( my $output );
46             $parser->parse_file($filename);
47             return [ $q->psgi_header( $content_types{$format} || "text/plain" ), [$output] ];
48         }
49         default {
50             die("Formato desconocido '$format'");
51         }
52     }
53 };
54 
55 sub make_link {
56     my $fmt = shift;
57     my $q   = shift;
58     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
59 }

La clausura recibe como parámetro el ambiente de PSGI (21) y lo utiliza para crear el objeto $q que usaremos como si fuera un objeto CGI. Esta clausura debe retornar un arreglo de dos elementos:
  1. Los encabezados: un arreglo de nombres y valores alternados
  2. El cuerpo: que debe ser un arreglo de líneas o un objeto IO::Handle

Una diferencia fundamental entre CGI::PSGI y CGI es que en el segundo se envía al navegador la salida estándar (STDOUT), mientras que en el primero, se retorna el cuerpo.

Así que la generación del contenido en la aplicación debe ser modificada. En el caso del código fuente (línea 26) ha quedado más simple, solo se retornan los encabezados junto con un objeto IO::Handle (creado con IO::File). y CGI::PSGI se encarga de leer los datos del objeto y enviarlos al navegador, de hecho en el caso de que sea un archivo real (como en este caso) y que el sistema operativo implemente sendfile(2) (como en mi caso que uso linux), el envío de los datos se realiza completamente en el kernel así que no habrá diferencia de eficiencia entre este programa y uno optimizado hecho en C (como apache).

En el caso de HTML (línea 29) he cambiado el uso de output_fh por output_string para que el contenido generado por Pod::Simple quede en $output, que se retorna en línea 39.

Como ya no se puede usar el STDOUT para enviar el contenido al navegador tampoco podemos utilizar el atajo $class->filter de Pod::Simple, así que lo he reemplazado por su equivalencia en las líneas 44 a 46 de la nueva aplicación.

Aunque tal vez no es obvio, el código retorna la clausura (línea 20) porque es el último valor que se calcula, ya que lo que sigue es una declaración.

Si llamamos a nuestro nuevo programa "server_pod" podremos arrancarlo con plackup de la siguiente manera:

$ plackup server_pod
Plack::Server::Standalone: Accepting connections at http://0:5000/

y podemos visualizar el contenido de los POD utilizando el navegador como ya se indicó, el servidor que utiliza plackup por defecto (Plack::Server::Standalone) es de un proceso de un solo hilo así que es ideal para el desarrollo o para una aplicación personal, pero si necesitas un servidor con calidad de producción debes ver otras opciones, la más recomendable para código que viene de CGI es probablemente Plack::Server::Standalone::Prefork, que puedes arrancar así:

$ plackup -s Standalone::Prefork server_pod
Plack::Server::Standalone: Accepting connections at http://0:5000/

Eso fue fácil, se utilizan valores por defecto para todo, pero si necesitas entonar el rendimiento del servidor puedes hacerlo dado opciones en la línea de comandos, las opciones generales se documentan en plackup y las de cada servidor en su clase respectiva.

Finalmente este código es mucho más eficiente que el del emulador del primer ejemplo porque no se necesita utilizar archivos temporales para capturar la salida estándar, aún así puede ejecutarse bajo Apache en modo CGI, FastCGI o incluso en mod-perl.

En una próxima ocasión mejoraré la aplicación utilizando directamente Plack y su middleware.

sábado, 21 de noviembre de 2009

Procesando POD con Pod::Simple

[English translation]
En el artículo anterior logramos convertir POD a HTML con relativa facilidad para un minúsculo servidor de documentos hecho con CGI, hoy voy a expandir el programa, permitiendo la visualización de documentos POD en una docena de formas diferentes.


Una opción útil cuando veo la documentación del CPAN, es la capacidad de mostrar las fuentes de los módulos, así que voy a agregar un enlace que permita visualizar la fuente de un documente, voy a poner el enlace al final del documento, configurando el footer de la conversión HTML, también debo agregar lógica para reconocer el nuevo tipo de enlaces.

Agregaré un parámetro de formato (format) al query, y lo interpretaré con el given de la línea 12, además para ser compatible con la versión anterior, permitiré que el formato sea opcional, y su valor por omisión será HTML (línea 11):

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use CGI;
 5 use CGI::Carp 'fatalsToBrowser';
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my $q        = new CGI;
10 my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
11 my $format   = $q->param("format") || "HTML";
12 given ($format) {
13     when ("source") {
14         print $q->header("text/plain");
15         open POD, $filename;
16         print $_ while (<POD>);
17     }
18     when ('HTML') {
19         my $parser = Pod::Simple::HTML->new;
20         print $q->header("text/html");
21         $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
22         my $footer = "<hr>" . make_link("source");
23         $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
24         $parser->output_fh(*STDOUT);
25         $parser->parse_file($filename);
26     }
27     default {
28         die("Formato desconocido '$format'");
29     }
30 }
31 
32 sub make_link {
33     my $fmt = shift;
34     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
35 }

Más del doble de las líneas de la versión anterior, sin embargo esta arquitectura muy pronto mostrará ser flexible en combinación con Pod::Simple y sus amigos. Mostrar las fuentes, en realidad es una tontería, solamente se envía un encabezado (línea 14) y luego el resto del archivo sin transformar.

La rutina make_link, facilita la creación de enlaces con el nuevo parámetro (format), a partir de la dirección que estamos visitando (ahora incluye también el query), y aunque solo se usa una vez (línea 22), la usaremos más a medida que agreguemos formatos de conversión a la aplicación.

Otro módulo que usé fue CGI::Carp con la opción "fatalsToBrowser", esto sirve para que los errores del programa se puedan ver en el navegador, si quieres probarlo, solo debes poner un parámetro format con algo desconocido para el programa y verás el mensaje de error en el navegador.

Una vez dicho esto voy a transformar POD a Wiki, para ello usaré "Pod::Simple::Wiki" que tiene conversores para al menos 9 formatos de wiki diferentes, así que sin importar si usas Mediawiki o Twiki, siempre puedes escribir tus artículos en POD :-)

Como Perl es dinámico, versatil y fácil voy a agregar todos los formatos de una vez, para lo cual requiero un arreglo con los formatos que voy a soportar (línea 9) y un mapa que me permita asociar cada formato con algún traductor de POD (línea 10):

 1 #!/usr/bin/perl
 2
 3 use Modern::Perl;
 4 use CGI;
 5 use CGI::Carp 'fatalsToBrowser';
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my @wikis   = qw(Usemod Twiki Template Kwiki Confluence Moinmoin Tiddlywiki Mediawiki Textile);
10 my %formats = (
11     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
12 );
13 
14 my $q        = new CGI;
15 my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
16 my $format   = $q->param("format") || "HTML";
17 given ($format) {
18     when ("source") {
19         print $q->header("text/plain");
20         open POD, $filename;
21         print $_ while (<POD>);
22     }
23     when ('HTML') {
24         my $parser = Pod::Simple::HTML->new;
25         print $q->header("text/html");
26         $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
27         my $footer = "<hr>" . make_link("source")
28             . " | Wiki formats: "
29             . join( " ", map { make_link($_) } @wikis );
30         $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
31         $parser->output_fh(*STDOUT);
32         $parser->parse_file($filename);
33     }
34     when (%formats) {
35         my $class = $formats{$format};
36         eval "require $class";
37         print $q->header( "text/plain" );
38         $class->filter($filename);
39     }
40     default {
41         die("Formato desconocido '$format'");
42     }
43 }
44 
45 sub make_link {
46     my $fmt = shift;
47     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
48 }

Todo el trabajo se hace al reconocer alguno de los nuevos formatos en la línea 34, allí obtenemos la clase de "Pod::Simple::Wiki" que implementa la conversión y la cargamos dinámicamente con require dentro de un eval (línea 36), de esta manera no tendremos que cargar todas las clases al principio de nuestro programa, utilizando sólo lo necesario para la conversión deseada, luego se envía el tipo de contenidos al navegador y se hace la conversión según clase de wiki deseado.

Ahora solo queda incluir los enlaces a los diferentes formatos en el footer, lo que se hace durante la generación de la página en HTML (líneas 27 a 29).

Si quisieras incluir parte de la documentación en un manual impreso probablemente querrás convertir POD, para las herramientas más adaptadas a este trabajo, vamos a generar RTF y LaTeX a partir de POD, no debe ser muy difícil porque ya existen las clases en el CPAN, lo primero es generalizar  el tipo de contenido enviado al navegador, permitiendo usar el mismo código para diversos formatos:

37         print $q->header( $content_types{$format} || "text/plain" );

Esto asume que existe un hash que tiene los formatos que manejaremos asociados a sus tipos de contenido, utilizaremos también este mapa para  generar los enlaces a los nuevos tipos de contenido:

27         my $footer = "<hr>" . make_link("source")
28             . join( " | ", map { make_link($_) } keys %content_types )
29             . " Wiki formats: "
30             . join( " | ", map { make_link($_) } @wikis );

El mapa de formatos y tipos de contenido se puede agregar al principio:

 9 my %content_types = (
10     RTF    => "application/rtf",
11     LaTeX  => "application/x-latex",
12 );

y no olvidemos que todo formato debe estar asociado a la clase que hace la conversión en %formats, de lo contrario el formato no será reconocido en el given:

14 my %formats = (
15     ( map { $_ => "Pod::Simple::$_" } keys %content_types ),
16     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
17 );

Ahora podemos convertir a RTF, lo que seguramente arrancará tu suite de oficina favorita, y en el caso de LaTeX probablemente se descargará el archivo.

Para cerrar, voy a incluir un último formato: PDF, este será más complejo porque no hay un módulo en el CPAN que transforme POD a PDF, por ello voy a hacerme uno, basado en Pod::Simple (línea 5), que use LaTeX como formato intermedio para crear los PDF.

 1 package Pod::Simple::PDF;
 2 
 3 use Modern::Perl;
 4 use Pod::Simple::LaTeX;
 5 use base "Pod::Simple";
 6 
 7 use File::Temp;
 8 use File::Spec::Functions;
 9 use IO::File;
10 use IO::Handle;
11 
12 sub new {
13     my $class = shift;
14     return bless { output_fh => \*STDOUT }, ref $class || $class;
15 }
16 
17 sub parse_file {
18     my $self = shift;
19     my $file = shift;
20 
21     my $dir      = File::Temp->newdir();
22     my $tex_name = catfile( $dir, "pod.tex" );
23     my $texf     = IO::File->new( $tex_name, "w" );
24     my $parser   = Pod::Simple::LaTeX->new;
25     $parser->output_fh($texf);
26     $parser->parse_file($file);
27     $texf->close;
28     `cd '$dir'; pdflatex '$tex_name'; pdflatex '$tex_name'`;
29     my $in = IO::File->new( catfile( $dir, "pod.pdf" ), "r" );
30     $self->{'output_fh'}->print($_) while readline($in);
31 }
32 
33 1;

Tal vez la razón por la cual no hay conversor a PDF es porque no hay una manera muy portable de hacerlo, yo voy usar la herramienta pdflatex que es parte de TeX Live, porque supongo que se puede instalar tanto en Unix como en Windows, sin embargo cualquier distribución moderna de TeX en unix debería incluir esta herramienta.

El método parse_file crea un directorio temporal usando File::Temp->newdir
para luego crear el archivo pod.tex dentro del directorio, que se usa para almacenar el resultado de la conversión realizada con Pod::Simple::LaTeX, este archivo ahora se procesa con el comando 'pdflatex' (línea 27) que produce el archivo 'pod.pdf' (y uno que otro archivo inútil) en el directorio temporal.

Muchas cosas pueden salir mal durante en la línea 27 porque el método simple que estoy ofrece muy poco control sobre lo que allí sucede, en una implementación mejorada habría que usar módulos como IPC::Run3 para controlar la ejecución de las herramientas y actuar correctamente ante las diversas fallas que pudieran ocurrir, sin embargo una de las caracteristicas interesantes de Perl es que se pueden hacer prototipos como este rápidamente y después se pueden refinar.

En las líneas 28 y 29 se transmite 'pod.pdf' al navegador, y al terminar el método parse file, la variable $dir sale de contexto y el objeto File::Temp se destruye, eliminando el directorio temporal y todo lo que este dentro del mismo.

Una vez que este módulo se guarda en el lugar apropiado, lo que hace automáticamente el CPAN si el módulo esta empaquetado según las instrucciones de perlmodlib (aunque ahora solo para probar puedes poner el archivo PDF.pm en el mismo directorio donde se encuentra el archivo HTML.pm que contiene Pod::Simple::HTML).

Finalmente hay que agregar el nuevo tipo de contenido (PDF) a la aplicación lo cual es tan simple como agregar una sola línea al hash %content_types (línea 12) sin necesidad de tocar mas nada, ya tenemos un servidor capaz de mostrar POD en mas de una docena de formatos:

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use CGI;
 5 use CGI::Carp 'fatalsToBrowser';
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my %content_types = (
10     RTF    => "application/rtf",
11     LaTeX  => "application/x-latex",
12     PDF    => "application/pdf",
13 );
14 my @wikis   = qw(Usemod Twiki Template Kwiki Confluence Moinmoin Tiddlywiki Mediawiki Textile);
15 my %formats = (
16     ( map { $_ => "Pod::Simple::$_" } keys %content_types ),
17     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
18 );
19 
20 my $q        = new CGI;
21 my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
22 my $format   = $q->param("format") || "HTML";
23 given ($format) {
24     when ("source") {
25         print $q->header("text/plain");
26         open POD, $filename;
27         print $_ while (<POD>);
28     }
29     when ('HTML') {
30         my $parser = Pod::Simple::HTML->new;
31         print $q->header("text/html");
32         $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
33         my $footer = "<hr>"
34             . join( " ", map { make_link($_) } "source", keys %content_types )
35             . " | Wiki formats: "
36             . join( " ", map { make_link($_) } @wikis );
37         $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
38         $parser->output_fh(*STDOUT);
39         $parser->parse_file($filename);
40     }
41     when (%formats) {
42         my $class = $formats{$format};
43         eval "require $class";
44         print $q->header( $content_types{$format} || "text/plain" );
45         $class->filter($filename);
46     }
47     default {
48         die("Formato desconocido '$format'");
49     }
50 }
51 
52 sub make_link {
53     my $fmt = shift;
54     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
55 }

domingo, 15 de noviembre de 2009

Herramientas de documentación en Perl

[English translation]
Perl tiene su propio formato de documentación llamado POD (del inglés Plain Old Documentation), este formato es estructurado y fué específicamente diseñado para ser manipulado fácilmente. POD se usa no solo como herramienta para la documentar Perl, sino como lenguaje de Wiki y hasta para escribir libros.

En perl la herramienta más conocida para leer la documentación es perldoc, que funciona de manera muy similar al manual de unix man(1), así cuando queremos ver la documentación de un módulo podemos hacer:

$ perldoc IO::Handle

Lo que nos mostrará el manual de IO::Handle, también podemos obtener el manual del módulo en otra variedad de formatos, por ejemplo, lo podemos guardar en formato "man", HTML o incluso LaTeX:

$ perldoc -T -o LaTeX IO::Handle > IO::Handle.tex
$ perldoc -T -o html IO::Handle > IO::Handle.html

Si vemos el HTML generado nos daremos cuenta que los enlaces generados al resto de la documentación se refieren al CPAN, pero esto es solo la forma en la que funciona perldoc, existen cientos de módulos para procesar POD, que permiten manipulaciones avanzadas y convertirlo a HTML, XML, LaTeX, texto y DocBook, entre otros.

Cuando se necesita mas control sobre la generación de los documentos, puedes usar otras herramientas como: pod2html y pod2latex que permiten crear documentos basados en varios archivos POD que se procesan en conjunto, por ejemplo para hacer un libro, donde cada capítulo se guarda en un POD diferente.

Si necesitas control total sobre la conversión de POD, siempre puedes programar utilizando los módulos disponibles en el CPAN, y uno de los más fáciles de usar es Pod::Simple, que ofrece varias conversiones predefinidas, por ejemplo si quieres generar HTML en una aplicación CGI, puedes hacerlo con facilidad:

1 use CGI;
2 use Pod::Simple::HTML;
3 
4 my $q = new CGI;
5 my $parser = Pod::Simple::HTML->new;
6 $parser->output_fh(*STDOUT);
7 
8 print $q->header("text/html");
9 $parser->parse_file("/usr/share/perl/5.8/IO/File.pod");

Este programa inicializa los objetos CGI y Pod::Simple::HTML (líneas 4 a 6), envía los encabezados de HTTP (línea 6) y finalmente envía el documento en HTML (línea 9).

En este caso debes conocer el nombre exacto del POD que deseas enviar, sin embargo si quieres saber el nombre de un archivo que contiene información sobre un módulo en particular, debes buscarlo, ¿donde?, pues lo más recomendable es buscarlo en los mismos sitios donde perl buscará los módulos y los ejecutables.

La variable @INC contiene los lugares donde perl busca los módulos que usamos en nuestros programas, esta es una combinación de lugares predefinidos al momento de compilar perl , el contenido de la variable de ambiente PERL5LIB y los lugares epecificados con "use lib" en el código de programas y módulos. Por otra parte cuando perl debe ejecutar un programa lo busca en la variable de ambiente PATH, así que para encontrar el archivo que contiene el POD de un módulo o programa en Perl  podemos usar una función como find_pod que se muestra a continuación:

 1 use Modern::Perl;
 2 use Env::Path;
 3 use File::Spec::Functions;
 4 
 5 sub find_pod
 6 {
 7     my $module = shift;
 8     my @module_path = split("::", $module);
 9     for my $dir ( @INC, Env::Path->PATH->List ) {
10         for my $ext ( '', '.pod', '.pm', '.pl' ) {
11             my $name = catfile($dir, @module_path) . $ext;
12             return $name if -e $name;
13         }
14     }
15     return undef;
16 }
17 
18 print "Nombre: ", find_pod(@ARGV), "\n";

Esta función recibe el nombre del módulo o programa, y lo separa por "::", luego itera todos los directorios que se encuentran en @INC y el PATH del sistema que convertimos en una lista con "Env::Path->PATH->List" en la línea 9, para cada directorio se buscan los nombres y los nombres aumentados con las extensiones: pod, pm y pl, el primer nombre encontrado es retornado, y si no se consigue nada se retorna undef.

Nótese el uso de "Env::Path" para obtener el path del sistema de manera portable y "File::Spec::Functions" que importa la función "catfile" para generar los nombres de archivo permite concatenar un nombre de archivo de manera portable, haciendo que esta subrutina funcione en unix y windows.

Sin embargo, hice esto solo por diversión, porque en CPAN ya hay algo mucho mejor: "Pod::Simple::Search", que está bien hecha, se puede instalar en cualquier lado y es mucho más flexible que la subrutina de juguete del ejemplo anterior, así que nuestro CGI para mostrar documentación en POD queda así:

 1 #!/usr/bin/perl
 2 use CGI;
 3 use Pod::Simple::HTML;
 4 use Pod::Simple::Search;
 5 
 6 my $q = new CGI;
 7 my $parser = Pod::Simple::HTML->new;
 8 $parser->output_fh(*STDOUT);
 9 
10 my $filename = Pod::Simple::Search->new->inc(1)->find($q->param("pod"));
11 print $q->header("text/html");
12 $parser->parse_file($filename);

Si tenemos un servidor web configurado, solamente copiamos el programa en el directorio apropiado para ejecutar CGI con el nombre perldocweb, lo marcamos como ejecutable, y todo debería funcionar, para probarlo podemos visitar:

http://localhost/cgi-bin/perldocweb?pod=IO::File

lo que muestra el manual de IO::File en el navegador, aunque todavía los enlaces apuntan al CPAN, si quieres que los enlaces apunten al mismo servidor de documentación hay que establecer el prefijo para generar los enlaces (perldoc_url_prefix), para ello utilizaré el método url() de CGI, que usado como se muestra en la línea 12 retorna el URL completo del script sin el query:


 1 #!/usr/bin/perl
 2 use CGI;
 3 use Pod::Simple::HTML;
 4 use Pod::Simple::Search;
 5 
 6 my $q = new CGI;
 7 my $parser = Pod::Simple::HTML->new;
 8 $parser->output_fh(*STDOUT);
 9 
10 my $filename = Pod::Simple::Search->new->inc(1)->find($q->param("pod"));
11 print $q->header("text/html");
12 $parser->perldoc_url_prefix($q->url(-path_info=>1) . "?pod=");
13 $parser->parse_file($filename);

Nada mal, un servidor de documentación sencillo en 13 líneas, en el próximo artículo veremos como convertir POD a otra cantidad de formatos, mientras tanto pueden instalar Pod::Server y ver como se hace esto con mucha más elegancia.

miércoles, 4 de noviembre de 2009

Manejando errores en Perl

[English Translation]

En Perl el manejo de excepciones es un poco diferente al que probablemente estamos acostumbrados, en particular Perl no tiene try/catch/throw como algunos otros lenguajes, pero no quiere decir que no hay manejo de excepciones, Perl puede capturar y manejar las excepciones tan bien como cualquier otro lenguaje solo que las estructuras son ligeramente diferentes.

El manejo de excepciones en Perl se basa en el uso del operador eval, que permite evaluar código y capturar errores, cuando eval recibe una cadena de caracteres, compila el código que esta en la cadena y lo ejecuta, sin embargo cualquier error que suceda en este código, desde la compilación hasta la ejecución abortará únicamente el eval y nuestro programa seguirá funcionando, por ejemplo:

1 use Modern::Perl;
2 my $result = eval( "5 / 0" );
3 say "El resultado es: $result";

Aunque el programa funciona, el resultado del eval es undef, porque la división por cero evitó que se retorne algún valor, esto además causa una advertencia en la línea 3 sobre la concatenación con una variable indefinida.

Lo que necesitamos es saber si el eval fué exitoso o no, y eso esta en la variable especial $@ (tambien conocida como $EVAL_ERROR si usamos el módulo English).

Así que para capturar la excepción solo verificamos $@ después del eval:

1 use Modern::Perl;
2 my $result = eval( "5 / 0" );
3 if ( $@ ) {
4     say "Ooops: $@";
5 }
6 else {
7     say "El resultado es: $result";
8 }

Lo que captura el error, el problema con esta solución es que el código que está dentro del string no se verifica a tiempo de compilación, sino que se compila al momento de ejecutarse, y aunque esto es sumamente poderoso, en la mayoría de los casos lo que nos interesa de eval es la capacidad de capturar errores, para ello la segunda forma de eval, recibe un bloque de código que se verifica durante la compilación del programa, y la podemos usar así:

2 my $result = eval { 5 / 0 };

En esta forma de eval las llaves ({}) marcan el bloque donde se requiere capturar excepciones y retorna la última expresión del bloque, o undef si sucede algún error de ejecución (porque los errores de sintaxis ya fueron capturados durante la compilación del programa).

La ultima primitiva que necesitamos para completar el sistema de excepciones de Perl es die, que permite lanzar una excepción, esta rutina recibe un valor que se asigna a la variable especial $@, así que podríamos hacer un programa que lanza una excepción así:

 1 use Modern::Perl;
 2 use IO::File;
 3 
 4 eval {
 5     my $fh = IO::File->new("AlgunArchivo.txt", "r");
 6     die("No se puede abrir") unless $fh;
 7 };
 8 if ( $@ ) {
 9     say "Ooops: $@";
10 }

Para algunos esta forma de capturar excepciones puede parecer arcáica, sin embargo, es tan buena como cualquier otra y con las facilidades de Perl podríamos utilizarla como base para implementar una estructura similar a la de otros lenguajes, es decir algo como try/catch. Como ya he comentado en otras oportunidades Perl es un lenguaje excelente para implementar nuevas características en base a las primitivas del lenguaje, y para divertirnos un rato podemos hacernos nuestra propia versión de try/catch:

 1 use Modern::Perl;
 2 use IO::File;
 3 
 4 sub try(&amp;) {
 5     eval { shift-&gt;() };
 6 }
 7 
 8 sub catch(&amp;) {
 9     if ( $@ ) {
10         local $_ = $@;
11         shift-&gt;();
12     }
13 }
14 
15 try {
16     my $fh = IO::File-&gt;new( "AlgunArchivo.txt", "r" );
17     die("No se puede abrir") unless $fh;
18 };
19 catch {
20     say "Ooops: $_";
21 };

Aquí el prototipo & de Perl permite hacer que las subrutinas try y catch reciban una clausura, pero el prototipo permite eliminar la declaración sub, aparentando que try y catch son estructuras de control que tiene bloque de código asociado.

Como en realidad son subrutinas cuyo primer parámetro es una clausura, se pueden invocar y así en la línea 5 se extrae el primer argumento (con shift) y se ejecuta la calusura (con ->()), todo dentro de un eval, si ocurre alguna excepción, se aborta el eval y se termina el try.

Cuando se usa catch después de un try, cualquier valor de $@ se localiza en $_ y se ejecuta la clausura, en la cual se puede usar $_ como valor de la excepción.

Para hacer una extensión que permita utilizar las primitivas recién creadas, solo tenemos que hacer un nuevo módulo, al que llamaré MyTryCatch y que debe estar en el archivo "MyTryCatch.pm":

 1 package MyTryCatch;
 2 
 3 use Exporter;
 4 
 5 our $VERSION = "1.000";
 6 our @EXPORT_OK = qw( try catch );
 7 our @EXPORT = @EXPORT_OK;
 8 
 9 sub try(&) {
10     eval { shift->() };
11 }
12 
13 sub catch(&) {
14     local $_ = $@;
15     shift->();
16 }
17 
18 1;

Luego cada vez que necesite usar la nueva estructura de control solo tengo que incluirla en un programa, por ejemplo:

 1 package MyTryCatch;
 2 
 3 use Exporter;
 4 
 5 our $VERSION = "1.000";
 6 our @EXPORT_OK = qw( try catch );
 7 our @EXPORT = @EXPORT_OK;
 8 
 9 sub try(&amp;) {
10     eval { shift-&gt;() };
11 }
12 
13 sub catch(&amp;) {
14     if ( $@ ) {
15         local $_ = $@;
16         shift-&gt;();
17     }
18 }
19
20 1;

Las primitivas de que acabamos de crear tienen algunos defectos, por ejemplo puede usarse catch sin try, y una instrucción return dentro de un bloque try o catch, se sale del bloque y no de la subrutina donde se declara la estructura de captura de excepciones, entre otros. Sin embargo con algo más de esfuerzo podríamos hacer una extensión que declare una estructura que se comporte mejor.

En el CPAN hay varios módulos que permiten manejar errores con estructuras similares, desde los más sencillos como Try::Tiny, que sufre de algunos de los inconvenientes de MyTryCatch hasta los más complejos como TryCatch que usa magia de la buena como Devel::Declare para hacer una estructura de manejo de excepciones con casi cualquier cosa que se te pueda imaginar.

Si tus requerimientos no son muy exigentes mi recomendación es utilizar Try::Tiny, es realmente minúscula, casi no tiene dependencias y es muy fácil de instalar, por otra parte si quieres un sistema de manejo de excepciones que hace de todo, no te importa mucho el consumo de recursos y tienes la paciencia para instalar docenas de módulos, puedes usar TryCatch.