135daf339ed8f56aafd2e436632651bf0d9e4142
[WWW-Mechanize-Script.git] / lib / Config / Any / WTScript.pm
blob135daf339ed8f56aafd2e436632651bf0d9e4142
1 package Config::Any::WTScript;
3 =head1 NAME
5 Config::Any::WTScript - Parse wtscript files.
7 =head1 SYNOPSIS
9 use Config::Any::WTScript;
11 my $tests = Config::Any::WTScript->parse($data);
13 =head1 DESCRIPTION
15 Parses a wtscript file and converts it to a set of test objects.
17 =head1 CLASS METHODS
19 =cut
21 use strict;
22 use warnings;
24 use vars qw($VERSION @extensions);
26 use base 'Config::Any::Base';
28 use Text::Balanced qw(extract_codeblock extract_delimited);
29 use File::Slurp qw(read_file);
31 use constant ST_FILE => 0;
32 use constant ST_TEST_BLOCK => 1;
34 $VERSION = '0.001_002';
36 # horizontal space regexp
37 my $reHS = qr/[\t ]/;
38 # sequence of any chars which doesn't contain ')', space chars and '=>'
39 my $reWORD = qr/(?: (?: [^=)\s] | [^)\s] (?!>) )+ )/x;
40 # eat comments regexp
41 my $reCOMMENT = qr/(?: \s*? ^ \s* \# .* )+/mx;
43 =head2 extensions(;@extensions)
45 When list of extensions to accept given, replace current list with given list.
47 Return an array of valid extensions (default: C<wts>, C<wtscript>).
49 =cut
51 @extensions = qw(wts wtscript);
53 sub extensions
55 my ( $class, @new_ext ) = @_;
56 @new_ext and @extensions = @new_ext;
57 return @extensions;
60 =head2 load( $file )
62 Parses wtscript text data passed in a scalar variable C<$data>.
64 =head3 Returns
66 A list of two elements - a reference to an array that contains test
67 objects and a reference to a hash that contains test parameters.
69 =cut
71 sub load
73 my $class = shift;
74 my $filename = shift;
76 my $data = read_file($filename);
78 my ( $tests, $opts ) = eval { _parse($data) };
80 if ($@)
82 my $exc = $@;
83 chomp $exc;
85 my $parse_pos = pos($data) || 0;
87 # find reminder of string near error (without surrounding
88 # whitespace)
89 $data =~ /\G $reHS* (.*?) $reHS* $/gmx;
90 my $near = $1;
91 if ( $near eq '' )
93 $near = 'at the end of line';
95 else
97 $near = "near '$near'";
100 # count lines
101 my $line_num = () = substr( $data, 0, $parse_pos ) =~ m|$|gmx;
102 pos($data) = $parse_pos;
103 $line_num-- if $data =~ /\G \z/gx;
105 die <<MSG;
106 Config::WebTest: wtscript parsing error
107 Line $line_num $near: $exc
111 my @configs;
112 foreach my $test_item (@$tests)
114 my %test = %$test_item;
115 my %cfg = (
116 request => {
117 agent => {},
118 method => 'get',
120 check => { response => 200, },
122 my $agent_cfg = $cfg{request}->{agent};
123 # convert params
124 defined( $test{user_agent} ) and $agent_cfg->{agent} = delete $test{user_agent};
125 defined( $test{handle_redirects} )
126 and $agent_cfg->{requests_redirectable} = delete $test{handle_redirects};
127 defined( $test{proxies} ) and $agent_cfg->{proxy} = { @{ delete $test{proxies} } };
128 defined( $agent_cfg->{requests_redirectable} )
129 and looks_like_number( $agent_cfg->{requests_redirectable} )
130 and $agent_cfg->{requests_redirectable} = [qw(GET POST)];
132 $cfg{request}->{uri} = delete $test{url};
133 defined $test{method} and $cfg{request}->{method} = lc delete $test{method};
134 defined $test{http_headers}
135 and $cfg{request}->{http_headers} = { @{ delete $test{http_headers} } };
137 $cfg{check} = \%test;
138 $cfg{opts} = $opts;
140 push( \@configs, \%cfg );
143 return \@configs;
146 sub eval_in_playground
148 my $code = shift;
150 return eval <<CODE;
151 package Config::WebTest::PlayGround;
153 no strict;
154 local \$^W; # aka no warnings in new perls
156 $code
157 CODE
160 sub make_sub_in_playground
162 my $code = shift;
164 return eval_in_playground("sub { local \$^W; $code }");
167 sub _parse
169 my $data = shift;
171 my $state = ST_FILE;
172 my $opts = {};
173 my $tests = [];
174 my $test = undef;
176 PARSER:
177 while (1)
179 # eat whitespace and comments
180 $data =~ /\G $reCOMMENT /gcx;
182 # eat whitespace
183 $data =~ /\G \s+/gcx;
185 if ( $state == ST_FILE )
187 if ( $data =~ /\G \z/gcx )
189 # end of file
190 last PARSER;
192 elsif ( $data =~ /\G test_name (?=\W)/gcx )
194 # found new test block start
195 $test = {};
196 $state = ST_TEST_BLOCK;
198 # find test block name
199 if ( $data =~ /\G $reHS* = $reHS* (?: \n $reHS*)?/gcx )
201 $test->{test_name} = _parse_scalar($data);
203 die "Test name is missing\n"
204 unless defined $test->{test_name};
207 else
209 # expect global test parameter
210 my ( $name, $value ) = _parse_param($data);
212 if ( defined $name )
214 _set_test_param( $opts, $name, $value );
216 else
218 die "Global test parameter or test block is expected\n";
222 elsif ( $state == ST_TEST_BLOCK )
224 if ( $data =~ /\G end_test (?=\W)/gcx )
226 push @$tests, $test;
227 $state = ST_FILE;
229 else
231 # expect test parameter
232 my ( $name, $value ) = _parse_param($data);
234 if ( defined $name )
236 _set_test_param( $test, $name, $value );
238 else
240 die "Test parameter or end_test is expected\n";
244 else
246 die "Unknown state\n";
250 return ( $tests, $opts );
253 sub _set_test_param
255 my $href = shift;
256 my $name = shift;
257 my $value = shift;
259 if ( exists $href->{$name} )
261 $href->{$name} = [ $href->{$name} ]
262 if ref( $href->{$name} )
263 and ref( $href->{$name} ) eq 'ARRAY';
264 push @{ $href->{$name} }, $value;
266 else
268 $href->{$name} = $value;
272 sub _parse_param
274 my $name;
276 if (
277 $_[0] =~ /\G ([a-zA-Z_]+) # param name
278 $reHS* = $reHS* (?: \n $reHS*)? # = (and optional space chars)
279 /gcx
282 $name = $1;
284 else
286 return;
289 my $value = _parse_value( $_[0] );
290 return unless defined $value;
292 return ( $name, $value );
295 sub _parse_value
297 if ( $_[0] =~ /\G \(/gcx )
299 # list elem
301 # ( scalar
302 # ...
303 # scalar )
305 # ( scalar => scalar
306 # ...
307 # scalar => scalar )
309 my @list = ();
311 while (1)
313 # eat whitespace and comments
314 $_[0] =~ /\G $reCOMMENT /gcx;
316 # eat whitespace
317 $_[0] =~ /\G \s+/gcx;
319 # exit loop on closing bracket
320 last if $_[0] =~ /\G \)/gcx;
322 my $value = _parse_value( $_[0] );
324 die "Missing right bracket\n"
325 unless defined $value;
327 push @list, $value;
329 if ( $_[0] =~ /\G $reHS* => $reHS* /gcx )
331 # handles second part of scalar => scalar syntax
332 my $value = _parse_value( $_[0] );
334 die "Missing right bracket\n"
335 unless defined $value;
337 push @list, $value;
341 return \@list;
343 else
345 # may return undef
346 return _parse_scalar( $_[0] );
350 sub _parse_scalar
352 my $parse_pos = pos $_[0];
354 if ( $_[0] =~ /\G (['"])/gcx )
356 my $delim = $1;
358 pos( $_[0] ) = $parse_pos;
359 my ($extracted) = extract_delimited( $_[0] );
360 die "Can't find string terminator \"$delim\"\n"
361 if $extracted eq '';
363 if ( $delim eq "'" or $extracted !~ /[\$\@\%]/ )
365 # variable interpolation impossible - just evalute string
366 # to get rid of escape chars
367 my $ret = eval_in_playground($extracted);
369 chomp $@;
370 die "Eval error\n$@\n" if $@;
372 return $ret;
374 else
376 # variable interpolation possible - evaluate as subroutine
377 # which will be used as callback
378 my $ret = make_sub_in_playground($extracted);
380 chomp $@;
381 die "Eval error\n$@\n" if $@;
383 return $ret;
386 elsif ( $_[0] =~ /\G \{/gcx )
388 pos( $_[0] ) = $parse_pos;
389 my ($extracted) = extract_codeblock( $_[0] );
390 die "Missing right curly bracket\n"
391 if $extracted eq '';
393 my $ret = make_sub_in_playground($extracted);
395 chomp $@;
396 die "Eval error\n$@\n" if $@;
398 return $ret;
400 else
402 $_[0] =~ /\G ((?: $reWORD $reHS+ )* $reWORD )/gcxo;
403 my $extracted = $1;
405 # may return undef
406 return $extracted;
410 =head1 COPYRIGHT
412 Copyright (c) 2001-2003 Ilya Martynov. All rights reserved.
414 This program is free software; you can redistribute it and/or modify
415 it under the same terms as Perl itself.
417 =head1 SEE ALSO
419 L<HTTP::WebTest|HTTP::WebTest>
421 L<HTTP::WebTest::API|HTTP::WebTest::API>
423 =cut