¿Puedo escribir un DSL en Perl?
(6)
Usamos Perl para la automatización de pruebas GUI. Ha sido muy exitoso. Hemos escrito un tipo de lenguaje DSL muy ligero para las pruebas GUI. El DSL es muy similar a un modelo de objetos.
Por ejemplo, tenemos un objeto Aplicación en la raíz. Cada hoja de propiedades en la aplicación es un objeto Ver. Cada página debajo de la página se llama el objeto Página en sí. Desde Perl enviamos comandos a una aplicación GUI y la GUI interpreta el comando y responde muy bien al comando. Para enviar un comando hacemos lo siguiente:
socket_object->send_command("App.View2.Page2.Activate()")
socket_object->send_command("App.View1.Page3.OKBtn.Click()")
Esto no es muy legible En cambio, quiero escribir un Perl DSL para la aplicación, vista y página. ¿Perl proporciona algún tipo de estructura de DSL donde puedo hacer lo siguiente?
App.View2.Page2.Activate();
App.View1.Page2.Click();
Donde la aplicación será una instancia de la clase de aplicación. Tengo que obtener el objeto de View2 en tiempo de ejecución.
¿Cómo usar tales cosas?
Llamadas de método en uso perl5 ->
no .
, por lo que se verá como App->View2->Page2->Activate()
o $App->View2->Page2->Active()
menos que haga algo realmente interesante (por ejemplo, un filtro de origen). Asumiendo que está bien, puedes usar cosas normales de Perl OO.
Ahora, la siguiente parte de lo que necesita es crear los métodos en tiempo de ejecución. Esto es realmente bastante simple:
sub _new_view {
my ($view, $view_num);
# ...
# ... (code to create $view object)
# ...
my $sym = "App::View$view_num";
*$sym = sub { return $view }; # can also use Symbol package
}
Alternativamente, si desea crear los métodos solo cuando son llamados, eso es lo que AUTOLOAD
hace. También puedes abusar de la carga automática para hacer que todas las llamadas al método tengan éxito (aunque ten cuidado con las que tienen un significado especial, como DESTROY).
Esto te dará la sintaxis. Hacer que tus objetos generen una cadena para pasar a send_command
no debería ser tan difícil.
Además, no estoy muy familiarizado con esto, pero es posible que desee consultar a Moose . Puede haber formas más fáciles de lograr esto.
Te recomiendo que dejes de hacer cosas raras "DSL" y simplemente escribas clases de Perl para manejar los objetos que deseas administrar. Te recomiendo que estudies el uso del nuevo sistema de objetos Moose Perl para esto, aunque el Perl OO tradicional estaría bien. Explore la documentación de Perl para ver los tutoriales de OO; son geniales.
Filtro de fuente DSL
Aquí hay otro intento. skiphoppy tiene un punto, pero en una segunda mirada, noté que (hasta ahora) no estabas preguntando mucho, que era tan complejo. Solo quiere tomar cada comando y decirle al servidor remoto que lo haga. No es perl lo que tiene que entender los comandos, es el servidor.
Por lo tanto, eliminé algunas de mis advertencias sobre los filtros de origen y decidí mostrarle cómo se puede escribir uno simple. Una vez más, lo que estás haciendo no es tan complejo, y mi "filtrado" a continuación es bastante fácil.
package RemoteAppScript;
use Filter::Simple; # The basis of many a sane source filter
use Smart::Comments; # treat yourself and install this if you don''t have
# it... or just comment it out.
# Simple test sub
sub send_command {
my $cmd = shift;
print qq(Command "$cmd" sent./n);
return;
}
# The list of commands
my @script_list;
# The interface to Filter::Simple''s method of source filters.
FILTER {
# Save $_, because Filter::Simple doesn''t like you reading more than once.
my $mod = $_;
# v-- Here a Smart::Comment.
### $mod
# Allow for whole-line perl style comments in the script
$mod =~ s/^/s*#.*$//m;
# 1. Break the package up into commands by split
# 2. Trim the strings, if needed
# 3. lose the entries that are just blank strings.
@script_list
= grep { length }
map { s/^/s+|/s+$//g; $_ }
split /;/, $mod
;
### @script_list
# Replace the whole script with a command to run the steps.
$_ = __PACKAGE__ . ''::run_script();'';
# PBP.
return;
};
# Here is the sub that performs each action.
sub run_script {
### @script_list
foreach my $command ( @script_list ) {
#send_command( $command );
socket_object->send_command( $command );
}
}
1;
RemoteAppScript.pm
guardar esto en RemoteAppScript.pm
en algún lugar donde su perl pueda encontrarlo. (prueba perl -MData::Dumper -e ''print Dumper( /@INC ), "/n"''
si necesitas saber dónde).
Entonces puedes crear un archivo "perl" que tenga esto:
use RemoteAppScript;
App.View2.Page2.Activate();
App.View1.Page2.Click();
sin embargo
No hay una razón real para que no pueda leer un archivo que contiene comandos del servidor. Eso arrojaría la llamada FILTER
. Tu tendrías
App.View2.Page2.Activate();
App.View1.Page2.Click();
en su archivo de script, y su archivo perl se vería más como esto:
#!/bin/perl -w
my $script = do {
local $/;
<ARGV>;
};
$script =~ s/^/s*#.*$//m;
foreach my $command (
grep { length() } map { s/^/s+|/s+$//g; $_ } split /;/, $script
) {
socket_object->send_command( $command );
}
Y llámalo así:
perl run_remote_script.pl remote_app_script.ras
Puedes hacer casi cualquier cosa en Perl. Pero tienes que hacer algunas cosas extrañas para que Perl funcione con sintaxis que simplemente no es Perl.
Para manejar exactamente lo que tienes allí, tendrás que hacer muchos trucos avanzados , que por definición no son fáciles de mantener. Usted tendría que:
- sobrecargar el operador de concatenación ''.'' (requiere una referencia bendita)
- desactive las restricciones o cree un submarino AUTOLOAD para permitir esas palabras simples; por supuesto, podría escribir subs para todas las palabras que quisiera usar (o usar el módulo de palabras clave ).
- posiblemente, cree paquetes múltiples, con múltiples
AUTOLOAD
s
Otra forma es filtros de fuente , probablemente pueda elegir un voto negativo solo por mencionar esta capacidad. Así que no recomendaría exactamente este enfoque para las personas que están pidiendo ayuda. Pero está afuera. Los filtros de origen (y yo he hecho mi parte) son solo una de esas áreas en las que puedes pensar que eres demasiado listo para tu propio bien.
Aún así, si está interesado en Perl como lenguaje "host" de DSL, los filtros de origen no están exactamente fuera de los límites. Sin embargo, al limitar esto a lo que usted muestra que desea hacer, Perl6 :: Attributes probablemente hará la mayor parte de lo que necesita de inmediato. Tomaría el
.
y traducirlos al "->" que Perl entendería. Pero aún puede echar un vistazo a los filtros de origen para comprender lo que sucede detrás de las escenas.Tampoco quiero dejar este tema sin sugerir que gran parte de la frustración que podría tener al generar su propio filtro fuente (lo que aconsejo que no se haga) se facilita al usar Filter :: Simple de Damian Conway.
Lo más simple es renunciar al ''.'' operador y simplemente espera código de aspecto Perl.
App->View2->Page2->Activate(); App->View1->Page2->Click();
App
sería un paquete o un sub. Ya sea definido en el paquete actual o importado que devuelve un objeto bendecido en un paquete con un sub deView2
(posiblemente un sub deAUTOLOAD
) que devuelve el nombre de un paquete o una referencia bendecida en un paquete, que comprendePage2
, y finalmente el el regreso de eso comprenderíaActivate
oActivate
Click
. (Consulte el tutorial de OO , si lo necesita).
Una alternativa para anular ''.''
o usando ->
sintaxis puede estar usando la sintaxis del paquete (: :), es decir, creando paquetes como App :: View2 y App :: View2 :: Page2 cuando se crea View2 / Page 2, agregando un sub de AUTOLOAD al paquete que delega en un App :: View :: Page o App :: Ver método, algo como esto:
En su aplicación / DSL.pm:
package App::DSL;
use strict;
use warnings;
# use to avoid *{"App::View::$view::method"} = /&sub and friends
use Package::Stash;
sub new_view(%);
our %views;
# use App::DSL (View1 => {attr1 => ''foo'', attr2 => ''bar''});
sub import {
my $class = shift;
my %new_views = @_ or die ''No view specified'';
foreach my $view (keys %new_views) {
my $stash = Package::Stash->new("App::View::$view");
# In our AUTOLOAD we create a closure over the right
# App::View object and call the right method on it
# for this example I just used _api_/L$method as the
# internal method name (Activate => _api_activate)
$stash->add_package_symbol(''&AUTOLOAD'' =>
sub {
our $AUTOLOAD;
my ($method) =
$AUTOLOAD =~ m{App::View::/Q$view/E::(.*)};
my $api_method = "_api_/L$method";
die "Invalid method $method on App::View::$view"
unless my $view_sub = App::View->can($api_method);
my $view_obj = $views{$view}
or die "Invalid View $view";
my $sub = sub {
$view_obj->$view_sub();
};
# add the function to the package, so that AUTOLOAD
# won''t need to be called for this method again
$stash->add_package_symbol("/&$method" => $sub);
goto $sub;
});
$views{$view} = bless $new_views{$view}, ''App::View'';
}
}
package App::View;
# API Method App::View::ViewName::Activate;
sub _api_activate {
my $self = shift;
# do something with $self here, which is the view
# object created by App::DSL
warn $self->{attr1};
}
1;
y en tu guion:
use strict;
use warnings;
# Create App::View::View1 and App::View::View2
use App::DSL (View1 => {attr1 => ''hello''}, View2 => {attr1 => ''bye''});
App::View::View1::Activate();
App::View::View2::Activate();
App::View::View1::Activate();
http://search.cpan.org/dist/Devel-Declare/ es una alternativa moderna a los filtros fuente que funciona integrando directamente en el analizador de perl, y vale la pena echarle un vistazo.