jueves, 24 de diciembre de 2009

Conociendo al alce (Moose)

En otras ocasiones les he hablado de de Moose, hoy voy a explicar lo más básico de este nuevo sistema de programación orientada a objetos que ha tomado por asalto al CPAN y que muchos desarrollos están adoptando como plataforma oficial de programación orientada a objetos.

Todo este ruido es porque Moose ofece mecanismos únicos de reutilización de código y polimorfismo que además son muy fáciles de utilizar. Así que ahora además de la herencia podrás utilizar roles (también conocidos como mixins), tendrás la capacidad de alterar fácilmente el comportamiento de una clase desde un rol, podrás delegar la de una clase en otra de manerá mágica, establecer contratos, inicialización perezosa, verificación de tipos y docenas de nuevas características que te harán reflexionar sobre como has podido programar toda tu vida sin ellas.

Directo del manual de Moose un ejemplo sencillo de implementación de herencia:

 1 package Point;
 2 use Moose;
 3 
 4 has 'x' => ( is => 'rw', isa => 'Int', default => 0 );
 5 has 'y' => ( is => 'rw', isa => 'Int', default => 0 );
 6 
 7 sub clear {
 8     my $self = shift;
 9     $self->x(0);
10     $self->y(0);
11 }
12 
13 sub distance_sqr {
14     my ($self, $point) = @_;
15     return ($self->x - $point->x)**2 + ($self->y - $point->y)**2;
16 }
17 
18 sub distance {
19     my ($self, $point) = @_;
20     return sqrt $self->distance_sqr( $point );
21 }
22 
23 1;

La línea 2 usa Moose, lo que ejecuta un compilador que transforma el programa, y que de paso activa el modo estricto (use strict) y las advertencias (use warnings).

Luego decimos que nuestro objeto tiene dos atributos (líneas 4 y 5) llamados x y y, estos atributos permiten la escritura y la lectura (is => "rw") y son de enteros (isa => "Int") y tienen un valor por defecto de 0 (default => 0), estas dos líneas generan los métodos de acceso a los atributos que además verifican la validez de los datos que se asignen a dichos atributos, también crean el código necesario para incializar los valores de los atributos en 0.

Luego se implementa una operación que borra el punto, es decir lo pone en (0,0) y otros que calculan la distancia y la distancia al cuadrado de este punto a otro.

Hasta ahora, puede parecer que Moose ahorra trabajo, sin embargo, siguiendo con el ejemplo veamos como sería un punto en 3 dimensiones:

 1 package Point3D;
 2 use Moose;
 3 
 4 extends 'Point';
 5 
 6 has 'z' => ( is => 'rw', isa => 'Int', default => 0 );
 7 
 8 after 'clear' => sub {
 9     my $self = shift;
10     $self->z(0);
11 };
12 
13 around 'distance_sqr' => sub {
14     my ($orig, $self, $point) = @_;
15     return  $self->$orig($point) + ($self->z - $point->z)**2;
16 };
17 
18 1;

Hacer una subclase es realmente simple solo se dice que esta clase extiende a otra (línea 4), se agrega un nuevo atributo y se agregan los métodos. Sin embargo Moose tiene interesantes capacidades que facilitan la reutilización del código, y es aquí donde se le empiza a ver el queso a la tostada.

En vez de escribir un nuevo método clear que sobreescriba el heredado, podemos agregarle un comportamiento al existente, así ni siquiera tenemos que invocar el método de la super clase.

En el caso de distance_sqr envolvemos el código de la super clase con nuestro método, lo que se parece un poco más a la manera tradicional de sobreescribir métodos para luego invocar al código de la super clase utilizando SUPER, sin embargo, estas operaciones le dan una gran potencia a un nuevo mecanismo de polimorfismo: los roles (también conocidos como mixins), del que hablaré en el próximo artículo.

Los objetos creados con Moose se utilizan como cualquier otro objeto, tomando en cuenta que los parámetros del constructor ahora son estilo hash:

1 use Point3D;
2 use Modern::Perl;
3 
4 my $p1 = Point3D->new;
5 my $p2 = Point3D->new( x => 1, y => 1, z => 1 );
6 
7 say "Distance: ", $p1->distance($p2);

Notese que la invocación $p1->distance($p2) invoca el método distance heredado de Point, que a su vez utiliza la envoltura distance_sqr de Point3D, que internamente invoca al método distance_sqr de Point.

lunes, 7 de diciembre de 2009

Rendimiento en PSGI/Plack

[English translation]
En mi articulo sobre PSGI aseveré que Plack era rápido, para demostrarlo comparé la velocidad del programa ejecutándose como CGI en apache (ACGI), como un servidor standalone en CGI::Emulate::PSGI (CEP) y como una aplicación nativa de PSGI.

La prueba no fue muy rigurosa, porque en realidad solo quería confirmar lo que había leído.

El comando para reportar la velocidad fue:

$ ab -n 1000 -c 10 -k "http://localhost:5000/cgi-bin/perldocweb?pod=PSGI&format=source"

Los resultados obtenidos fueron:

ACGI
CEP
PSGI
Peticiones/seg.
10.57
267.17
512.31
Tiempo de ejecución (ms)
94.618
3.743
1.952
Rata de transf. (kBps)
179.52
4539.79
8686.67

Solo por ver la velocidad cruda, me hice un pequeño programa para servir archivos de texto y comparar el rendimiento contra apache sirviendo archivos estáticos:

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use IO::File;
 5 
 6 my $dir = "/home/jrey/htdocs";
 7 
 8 my $app = sub {
 9     my $env      = shift;
10     my $filename = $dir . $env->{'REQUEST_URI'};
11     return [ '200', ['Content-Type' => "text/plain"], IO::File->new($filename) ];
12 };

Los resultados para el comando:

$ ab -n 1000 -c 10 -k "http://localhost:5000/PSGI.pod"

Fueron:

Plackup
Apache
Peticiones/seg.
614.69
3217.03
Tiempo de ejecución (ms)
1.627
0.311
Rata de transf. (kBps)
10425.21
55133.41

Como dije antes, Plack es muy rápido, y en particular esta prueba muestra que el rendimiento es aceptable incluso para contenido estático, así que podremos desplegar las aplicaciones directamente en perl, sin necesidad de un servidor web adicional, excepto para circunstancias especiales, como alta disponibilidad y balanceo de carga, en cuyo caso también hay algunas soluciones en Perl como perlbal. ¿ya les mencioné existe PSGI para perlbal?

domingo, 6 de diciembre de 2009

Error en CGI::Emulate::PSGI

Mientras trabajaba con el código del articulo anterior, me dí cuenta de que en realidad el ejemplo sobre CGI::Emulate::PSGI no funcionaba correctamente, debido a que no reinicié las variables globales de CGI.

1 use CGI::Emulate::PSGI;
2 use CGI;
3 
4 my $app = CGI::Emulate::PSGI->handler(sub {
5     CGI::initialize_globals();
6     do "perldocweb";
7 })

De otro modo los parámetros del primer request se quedan fijos para siempre.

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.