Могу ли я написать DSL на Perl? - PullRequest
       16

Могу ли я написать DSL на Perl?

12 голосов
/ 05 декабря 2008

Мы используем Perl для автоматизации тестирования GUI. Это было очень успешно. Мы написали очень легкий язык DSL для тестирования GUI. DSL очень похож на объектную модель.

Например, у нас есть объект Application в корне. Каждый лист свойств в приложении является объектом View. Каждая страница под страницей называется самим объектом Page. Из Perl мы отправляем команды в приложение с графическим интерфейсом, а графический интерфейс интерпретирует команду и хорошо отвечает на команду. Для отправки команды мы делаем следующее:

socket_object->send_command("App.View2.Page2.Activate()")
socket_object->send_command("App.View1.Page3.OKBtn.Click()")

Это не очень читабельно. Вместо этого я хочу написать Perl DSL для App, View и Page. Предоставляет ли Perl некую структуру DSL, где я могу сделать следующее?

App.View2.Page2.Activate();
App.View1.Page2.Click();

Где App должно быть экземпляром класса Application. Я должен получить объект View2 во время выполнения.

Как пользоваться такими вещами?

Ответы [ 6 ]

20 голосов
/ 05 декабря 2008

Вы можете делать почти все в Perl. Но вы должны сделать некоторые странные вещи, чтобы заставить Perl работать с синтаксисом, отличным от Perl.

  • Чтобы справиться именно с тем, что у вас есть, вам понадобится множество продвинутых трюков, которые по определению не так уж и ремонтопригодны. Вы должны были бы:

    • перегрузка оператор объединения '.' (требуется благословенная ссылка)
    • отключите ограничения или создайте подпрограммы AUTOLOAD , чтобы учесть эти голые слова - конечно, вы можете написать подпрограммы для всех слов, которые хотите использовать (или использовать модуль голых слов ).
    • возможно, создать несколько пакетов с несколькими AUTOLOAD s
  • Другой способ - это исходные фильтры , я, вероятно, могу поднять понижение только для , упомянув эту возможность. Поэтому я бы не рекомендовал этот подход людям, которые просят о помощи. Но это там. Исходные фильтры (и я сделал свою долю) - это только одна из тех областей, где вы можете думать, что вы слишком умны для своего же блага.

    Тем не менее, если вас интересует Perl в качестве языка "хоста" DSL, тогда исходные фильтры не являются полностью запрещенными. Однако, ограничивая это только тем, что вы показываете, что вы хотите сделать, Perl6 :: Attributes , вероятно, сделает большую часть того, что вам нужно, прямо с полки. Нужно взять . и перевести их в "->", что Perl поймет. Но вы все равно можете посмотреть на исходные фильтры, чтобы понять что происходит за кулисами.

    Я также не хочу уходить из этой темы, не предполагая, что большая часть разочарований, которые вы могли бы испытать, создав собственный фильтр исходного кода (что я советую НЕ делать), ослаблена с помощью Filter :: Simple Дамиана Конвея .

  • Самое простое - отказаться от «.» оператор и вместо этого ожидать Perl-выглядящий код.

    App->View2->Page2->Activate(); 
    App->View1->Page2->Click();
    

    App будет либо пакетом, либо подпрограммой. Определенный в текущем пакете или импортированный, который возвращает объект, благословленный в пакет с подпрограммой View2 (возможно, подпункт AUTOLOAD), которая возвращает либо имя пакета, либо ссылку, благословленную в пакет, который понимает Page2, и, наконец, возврат из этого будет понимать Activate или Click. (См. Учебник OO , если вам нужно.)

6 голосов
/ 05 декабря 2008

Я рекомендую вам прекратить попытки делать причудливые вещи "DSL" и просто написать классы Perl для обработки объектов, которыми вы хотите управлять. Я рекомендую вам использовать для этого новую объектную систему Moose Perl, хотя традиционный Perl OO был бы идеальным вариантом. Копаться в документации по Perl для руководств по ОО; они великолепны.

4 голосов
/ 06 декабря 2008

Фильтр источника DSL

Вот еще одна попытка. У skiphoppy есть смысл, но при втором взгляде я заметил, что (пока) вы не спрашивали много, что было таким сложным. Вы просто хотите принять каждую команду и сказать удаленному серверу сделать это. Это не perl , который должен понимать команды, это сервер.

Итак, я удалил некоторые из моих предупреждений об исходных фильтрах и решил показать вам как просто можно написать. Опять же, то, что вы делаете, не так сложно, и моя «фильтрация» ниже довольно проста.

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 где-нибудь, где ваш Perl сможет его найти. (попробуйте perl -MData::Dumper -e 'print Dumper( \@INC ), "\n"', если вам нужно знать, где.)

Затем вы можете создать файл "perl", который имеет это:

use RemoteAppScript;
App.View2.Page2.Activate();
App.View1.Page2.Click();

Однако

Нет реальной причины, по которой вы не можете прочитать файл, содержащий команды сервера. Это выбросило бы вызов FILTER. Вы бы

App.View2.Page2.Activate();
App.View1.Page2.Click();

в вашем файле сценария, и ваш Perl-файл будет выглядеть примерно так:

#!/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 );
}

И назовите это так:

perl run_remote_script.pl remote_app_script.ras
4 голосов
/ 05 декабря 2008

В вызовах методов в perl5 используется ->, а не ., поэтому он будет выглядеть как App->View2->Page2->Activate() или $App->View2->Page2->Active(), если вы не сделаете что-то действительно интересное (например, фильтр источника). Предполагая, что это нормально, вы можете использовать обычные Perl OO.

Теперь следующая часть того, что вам нужно, - это создание методов во время выполнения. На самом деле это довольно просто:

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
}

В качестве альтернативы, если вы хотите создавать методы только тогда, когда они вызываются, это то, что делает AUTOLOAD. Вы также можете использовать автозагрузку, чтобы все вызовы методов выполнялись успешно (хотя следите за вызовами со специальными значениями, такими как DESTROY).

Это даст вам синтаксис. Наличие ваших объектов генерирует строку для передачи send_command не должно быть так сложно.

Кроме того, я не слишком знаком с этим, но вы можете проверить Moose . Возможно, есть более простые способы сделать это.

1 голос
/ 21 октября 2010

http://search.cpan.org/dist/Devel-Declare/ - современная альтернатива исходным фильтрам, которая работает при интеграции непосредственно в Perl-парсер и заслуживает внимания.

0 голосов
/ 21 октября 2010

Альтернативой переопределению '.' или использованию синтаксиса -> может быть использование синтаксиса пакета (: :), то есть создание пакетов, таких как App :: View2 и App :: View2 :: Page2, когда создается View2 / Page 2, добавление подпрограммы AUTOLOAD в пакет, который делегируется методу App :: View :: Page или App :: View, что-то вроде этого:

В вашем приложении / 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;

и в вашем скрипте:

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();
...