Commit def6a886 authored by matsduf's avatar matsduf
Browse files

Merge pull request #3 from mattias-p/v1.2.0

v1.2.0
parents 68f1fa2e bfffbc2f
......@@ -14,3 +14,5 @@ PDT-TS-Whois-*
Makefile.PL
META.json
META.yml
*~
.DS_Store
......@@ -24,9 +24,12 @@ Version history
===============
* v1.0.0 - Initial public release (2015-12-03)
* v1.1.0 - Updated public release (2016-01-08)
* v1.2.0 - Updated public release (2016-02-02)
The v1.1.0 release primarily matches the updates to the PDT Whois TP and TCs in the version 2.9 document release. It also handles the issue with IDN in the v1.0.0 release and corrects found bugs.
The v1.2.0 release primarily matches the updates to the PDT Whois TP in the the version 2.10 document release. It also corrects found bugs.
Specification compatibility matrix
----------------------------------
Refer to this compatibility matrix when deciding which version of Whois Selftest
......@@ -45,6 +48,10 @@ Tool to use.
<td>v1.1.0</td>
<td>v.2.9</td>
</tr>
<tr>
<td>v1.2.0</td>
<td>v.2.10</td>
</tr>
</table>
Roadmap
......@@ -96,7 +103,7 @@ other OSs.
Installation
============
Clone the project repository and choose version according to the specification
compatibility matrix.
compatibility matrix. In the normal case, choose the latest version.
$> git clone https://github.com/dotse/Whois-Selftest-Tool.git <srcdir>
$> cd <srcdir>
......@@ -109,10 +116,12 @@ Install Whois Selftest Tool scripts and libraries.
$> ./Build test
$> ./Build install
To check the installation run the scripts with `--help`.
To check the installation run the scripts with `--help`. Before the whois-test
script can be run, the EPP database must be fetched.
$> whois-test --help
$> whois-fetch-epp-repo-ids --help
$> whois-fetch-epp-repo-ids
$> whois-test --help
After installing, you can find documentation for this module with the
perldoc command.
......
libwhois-selftest-tool-perl (1.2.0-1) unstable; urgency=low
* Matches the updates to the PDT Whois TP in the version 2.10 document release
* Corrects found bugs
-- Mattias Päivärinta <mattias.paivarinta@doxwork.com> Tue, 02 Feb 2016 16:00:00 +0100
libwhois-selftest-tool-perl (1.1.0-1) unstable; urgency=low
* Matches the updates to the PDT Whois TP and TCs in the version 2.9 document release
......
......@@ -4,7 +4,7 @@ use strict;
use warnings;
use 5.014;
use version; our $VERSION = qv( 1.1.0 );
use version; our $VERSION = qv( 1.2.0 );
=pod
......
......@@ -63,13 +63,12 @@ A subrule value HASHREF may have the following keys:
=over 4
=item optional
=item quantifier
Values: 'y'|'n' (default: n)
=item repeatable
Values: non-negative integer|'unbounded' (default: 1)
Values: 'required' | 'required-strict' | 'optional-free' |
'optional-not-empty' | 'optional-constrained' | 'empty-constrained' |
'omitted-constrained' | /repeatable (max \d+)?/ |
/optional-repeatable (max \d+)?/ (default: required)
=item line
......@@ -104,8 +103,8 @@ Registrar Object query:
- Registrar reply: { }
Domain name reply:
- Domain name details section: { }
- Domain name subsection 1: { optional: free }
- Empty line: { repeatable: 3, line: empty line }
- Domain name subsection 1: { quantifier: optional-free }
- Empty line: { quantifier: repeatable max 3, line: empty line }
- AWIP footer: { }
- Legal disclaimer: { }
Domain name subsection 1:
......@@ -125,9 +124,9 @@ Domain name subsection 5:
Domain name subsection 2: { }
Registrar reply:
- Registrar details section: { }
- Registrar subsection 1: { optional: free }
- Empty line: { repeatable: 3, line: empty line }
- AWIP footer: { optional: free }
- Registrar subsection 1: { quantifier: optional-free }
- Empty line: { quantifier: repeatable max 3, line: empty line }
- AWIP footer: { quantifier: optional-free }
- Legal disclaimer: { }
Registrar subsection 1:
Last updated footer: { }
......@@ -146,17 +145,17 @@ Registrar subsection 5:
Registrar subsection 2: { }
Name server reply type 1:
- Name server details section: { }
- Name server subsection 1: { optional: free }
- Empty line: { repeatable: 3, line: empty line }
- AWIP footer: { optional: free }
- Name server subsection 1: { quantifier: optional-free }
- Empty line: { quantifier: repeatable max 3, line: empty line }
- AWIP footer: { quantifier: optional-free }
- Legal disclaimer: { }
Name server details section:
- Server Name: { line: field, type: query name server }
- IP Address: { optional: free, repeatable: unbounded, line: field, type: query name server ip }
- Registrar: { optional: constrained, line: field, type: postal line }
- WHOIS Server: { optional: constrained, line: field, type: hostname }
- Referral URL: { optional: constrained, line: field, type: http url }
- Additional fields section: { optional: free }
- IP Address: { quantifier: optional-repeatable, line: field, type: query name server ip }
- Registrar: { quantifier: optional-constrained, line: field, type: postal line }
- WHOIS Server: { quantifier: optional-constrained, line: field, type: hostname }
- Referral URL: { quantifier: optional-constrained, line: field, type: http url }
- Additional field: { quantifier: optional-repeatable, line: field, keytype: name server object additional field key }
Name server subsection 1:
Last updated footer: { }
Name server subsection 2: { }
......@@ -174,132 +173,148 @@ Name server subsection 5:
Name server subsection 2: { }
Name server reply type 2:
- Multiple name servers section: { }
- Empty line: { optional: free, repeatable: 3, line: empty line }
- Empty line: { quantifier: optional-repeatable max 3, line: empty line }
- Last updated footer: { }
- Empty line: { repeatable: 3, line: empty line }
- AWIP footer: { optional: free }
- Empty line: { quantifier: repeatable max 3, line: empty line }
- AWIP footer: { quantifier: optional-free }
- Legal disclaimer: { }
Registrar details section:
- Registrar Name: { line: field, type: query registrar name }
- Street: { line: field, type: postal line, repeatable: unbounded }
- Street: { line: field, type: postal line, quantifier: repeatable }
- City: { line: field, type: postal line }
- State/Province: { optional: constrained, line: field, type: postal line }
- Postal Code: { optional: constrained, line: field, type: postal code }
- State/Province: { quantifier: optional-constrained, line: field, type: postal line }
- Postal Code: { quantifier: optional-constrained, line: field, type: postal code }
- Country: { line: field, type: country code }
- Phone Number: { line: field, type: phone number }
- Phone Ext: { optional: free, line: field, type: token }
- Fax number section: { optional: free }
- Email: { line: field, type: email address }
- WHOIS Server: { optional: constrained, line: field, type: hostname }
- Phone number section: { quantifier: repeatable }
- Fax number section: { quantifier: required }
- Email: { quantifier: repeatable, line: field, type: email address }
- WHOIS Server: { quantifier: optional-constrained, line: field, type: hostname }
- Referral URL: { line: field, type: http url }
- Admin contact section: { optional: free, repeatable: unbounded }
- Technical contact section: { optional: free, repeatable: unbounded }
- Additional fields section: { optional: free }
- Admin contact section: { quantifier: optional-repeatable }
- Technical contact section: { quantifier: optional-repeatable }
- Additional field: { quantifier: optional-repeatable, line: field, keytype: registrar object additional field key }
Admin contact section:
- Admin Contact: { line: field, type: postal line }
- Phone number section: { repeatable: unbounded }
- Fax number section: { optional: free, repeatable: unbounded }
- Email: { line: field, type: email address, repeatable: unbounded }
- Phone number section: { quantifier: repeatable }
- Fax number section: { quantifier: required }
- Email: { line: field, type: email address, quantifier: repeatable }
Technical contact section:
- Technical Contact: { line: field, type: postal line }
- Phone number section: { repeatable: unbounded }
- Fax number section: { optional: free, repeatable: unbounded }
- Email: { line: field, type: email address, repeatable: unbounded }
- Phone number section: { quantifier: repeatable }
- Fax number section: { quantifier: required }
- Email: { line: field, type: email address, quantifier: repeatable }
Phone number section:
- Phone Number: { line: field, type: phone number }
- Phone Ext: { optional: free, line: field, type: token }
- Phone Ext: { quantifier: optional-free, line: field, type: token }
Fax number section:
- Fax Number: { line: field, type: phone number }
- Fax Ext: { optional: free, line: field, type: token }
Fax number section type A: { quantifier: repeatable }
Fax number section type B: { quantifier: required }
Fax number section type C: { quantifier: required }
Fax number section type A:
- Fax Number: { line: field, type: phone number, quantifier: required-strict }
- Fax Ext: { line: field, type: token, quantifier: optional-free }
Fax number section type B:
- Fax Number: { line: field, type: void, quantifier: empty-constrained }
- Fax Ext: { line: field, type: token, quantifier: optional-free }
Fax number section type C:
- Fax Number: { line: field, type: void, quantifier: omitted-constrained }
Domain name details section:
- Domain Name: { line: field, type: query domain name }
- Internationalized Domain Name: { optional: free, line: field, type: u-label }
- Internationalized Domain Name: { quantifier: optional-free, line: field, type: u-label }
- Domain ID: { line: field, type: epp repo id }
- WHOIS Server: { optional: constrained, line: field, type: hostname }
- WHOIS Server: { quantifier: optional-constrained, line: field, type: hostname }
- Referral URL: { line: field, type: http url }
- Updated Date: { optional: constrained, line: field, type: time stamp }
- Updated Date: { quantifier: optional-constrained, line: field, type: time stamp }
- Creation Date: { line: field, type: time stamp }
- Registry Expiry Date: { line: field, type: time stamp }
- Sponsoring Registrar: { line: field, type: token }
- Sponsoring Registrar IANA ID: { line: field, type: positive integer }
- Domain Status: { repeatable: unbounded, line: field, type: domain status }
- Domain Status: { quantifier: repeatable, line: field, type: domain status }
- Registrant ID: { line: field, type: token }
- Registrant Name: { line: field, type: postal line }
- Registrant Organization: { optional: constrained, line: field, type: postal line }
- Registrant Street: { repeatable: unbounded, line: field, type: postal line }
- Registrant Organization: { quantifier: optional-constrained, line: field, type: postal line }
- Registrant Street: { quantifier: repeatable, line: field, type: postal line }
- Registrant City: { line: field, type: postal line }
- Registrant State/Province: { optional: constrained, line: field, type: postal line }
- Registrant Postal Code: { optional: constrained, line: field, type: postal code }
- Registrant State/Province: { quantifier: optional-constrained, line: field, type: postal line }
- Registrant Postal Code: { quantifier: optional-constrained, line: field, type: postal code }
- Registrant Country: { line: field, type: country code }
- Registrant Phone: { line: field, type: phone number }
- Registrant Phone Ext: { optional: constrained, line: field, type: token }
- Registrant Fax: { optional: constrained, line: field, type: phone number }
- Registrant Fax Ext: { optional: constrained, line: field, type: token }
- Registrant Phone Ext: { quantifier: optional-constrained, line: field, type: token }
- Registrant Fax: { quantifier: optional-constrained, line: field, type: phone number }
- Registrant Fax Ext: { quantifier: optional-constrained, line: field, type: token }
- Registrant Email: { line: field, type: email address }
- Admin ID: { line: field, type: token }
- Admin Name: { line: field, type: postal line }
- Admin Organization: { optional: constrained, line: field, type: postal line }
- Admin Street: { repeatable: unbounded, line: field, type: postal line }
- Admin Organization: { quantifier: optional-constrained, line: field, type: postal line }
- Admin Street: { quantifier: repeatable, line: field, type: postal line }
- Admin City: { line: field, type: postal line }
- Admin State/Province: { optional: constrained, line: field, type: postal line }
- Admin Postal Code: { optional: constrained, line: field, type: postal code }
- Admin State/Province: { quantifier: optional-constrained, line: field, type: postal line }
- Admin Postal Code: { quantifier: optional-constrained, line: field, type: postal code }
- Admin Country: { line: field, type: country code }
- Admin Phone: { line: field, type: phone number }
- Admin Phone Ext: { optional: constrained, line: field, type: token }
- Admin Fax: { optional: constrained, line: field, type: phone number }
- Admin Fax Ext: { optional: constrained, line: field, type: token }
- Admin Phone Ext: { quantifier: optional-constrained, line: field, type: token }
- Admin Fax: { quantifier: optional-constrained, line: field, type: phone number }
- Admin Fax Ext: { quantifier: optional-constrained, line: field, type: token }
- Admin Email: { line: field, type: email address }
- Tech ID: { line: field, type: token }
- Tech Name: { line: field, type: postal line }
- Tech Organization: { optional: constrained, line: field, type: postal line }
- Tech Street: { repeatable: unbounded, line: field, type: postal line }
- Tech Organization: { quantifier: optional-constrained, line: field, type: postal line }
- Tech Street: { quantifier: repeatable, line: field, type: postal line }
- Tech City: { line: field, type: postal line }
- Tech State/Province: { optional: constrained, line: field, type: postal line }
- Tech Postal Code: { optional: constrained, line: field, type: postal code }
- Tech State/Province: { quantifier: optional-constrained, line: field, type: postal line }
- Tech Postal Code: { quantifier: optional-constrained, line: field, type: postal code }
- Tech Country: { line: field, type: country code }
- Tech Phone: { line: field, type: phone number }
- Tech Phone Ext: { optional: constrained, line: field, type: token }
- Tech Fax: { optional: constrained, line: field, type: phone number }
- Tech Fax Ext: { optional: constrained, line: field, type: token }
- Tech Phone Ext: { quantifier: optional-constrained, line: field, type: token }
- Tech Fax: { quantifier: optional-constrained, line: field, type: phone number }
- Tech Fax Ext: { quantifier: optional-constrained, line: field, type: token }
- Tech Email: { line: field, type: email address }
- Billing contact section: { optional: free }
- Name server section: { repeatable: unbounded }
- Billing contact section: { quantifier: optional-free }
- Name server section: { }
- DNSSEC: { line: field, type: dnssec }
- Additional fields section: { optional: free }
- Additional field: { quantifier: optional-repeatable, line: field, keytype: domain name object additional field key }
Billing contact section:
- Billing ID: { line: field, type: token }
- Billing Name: { line: field, type: postal line }
- Billing Organization: { optional: free, line: field, type: postal line }
- Billing Street: { repeatable: unbounded, line: field, type: postal line }
- Billing Organization: { quantifier: optional-free, line: field, type: postal line }
- Billing Street: { quantifier: repeatable, line: field, type: postal line }
- Billing City: { line: field, type: postal line }
- Billing State/Province: { optional: free, line: field, type: postal line }
- Billing Postal Code: { optional: free, line: field, type: postal code }
- Billing State/Province: { quantifier: optional-free, line: field, type: postal line }
- Billing Postal Code: { quantifier: optional-free, line: field, type: postal code }
- Billing Country: { line: field, type: country code }
- Billing Phone: { line: field, type: phone number }
- Billing Phone Ext: { optional: free, line: field, type: token }
- Billing Fax: { optional: free, line: field, type: phone number }
- Billing Fax Ext: { optional: free, line: field, type: token }
- Billing Phone Ext: { quantifier: optional-free, line: field, type: token }
- Billing Fax: { quantifier: optional-free, line: field, type: phone number }
- Billing Fax Ext: { quantifier: optional-free, line: field, type: token }
- Billing Email: { line: field, type: email address }
Name server section:
- Name Server: { optional: constrained, line: field, type: hostname }
- IP Address: { optional: free, repeatable: unbounded, line: field, type: ip address }
Name server section type A: { quantifier: repeatable }
Name server section type B: { quantifier: repeatable }
Name server section type C: { quantifier: repeatable }
Name server section type A:
- Name Server: { quantifier: required-strict, line: field, type: hostname }
- IP address section: { quantifier: repeatable }
Name server section type B:
- Name Server: { quantifier: empty-constrained, line: field, type: void }
Name server section type C:
- Name Server: { quantifier: omitted-constrained, line: field, type: void }
IP address section:
- IP Address: { quantifier: optional-not-empty, line: field, type: ip address }
Multiple name servers section:
- Multiple name servers line: { line: multiple name servers line }
- ROID line: { line: roid line }
- ROID line: { line: roid line, repeatable: unbounded }
Additional fields section:
- Additional field: { repeatable: unbounded, line: field }
- ROID line: { line: roid line, quantifier: repeatable }
Last updated subsection 1:
Last updated footer: { }
Last updated subsection 2: { }
Last updated subsection 2:
- Empty line: { repeatable: 2, line: empty line }
- Empty line: { quantifier: repeatable max 2, line: empty line }
- Last updated footer: { }
Last updated footer:
- Last update line: { line: last update line }
AWIP footer:
- AWIP line: { line: awip line }
- Empty line: { repeatable: 3, line: empty line }
- Empty line: { quantifier: repeatable max 3, line: empty line }
Legal disclaimer:
- Non-empty line: { line: non-empty line }
- Any line: { optional: free, repeatable: unbounded, line: any line }
- Any line: { quantifier: optional-repeatable, line: any line }
......@@ -6,6 +6,7 @@ use 5.014;
use Carp;
use English;
use Readonly;
use URI;
use Regexp::IPv6;
......@@ -43,10 +44,14 @@ The default types are:
* token
* translation clause
* u-label
* domain name object additional field key
* registrar object additional field key
* name server object additional field key
* void
=cut
my %domain_status_codes = (
Readonly my %DOMAIN_STATUS_CODES => (
addPeriod => 1,
autoRenewPeriod => 1,
clientDeleteProhibited => 1,
......@@ -72,6 +77,85 @@ my %domain_status_codes = (
transferPeriod => 1,
);
Readonly my %DOMAIN_NAME_ADDITIONAL_FIELD_KEY_BLACKLIST => (
'Domain Name' => 1,
'Domain ID' => 1,
'WHOIS Server' => 1,
'Referral URL' => 1,
'Updated Date' => 1,
'Creation Date' => 1,
'Registry Expiry Date' => 1,
'Sponsoring Registrar' => 1,
'Sponsoring Registrar IANA ID' => 1,
'Domain Status' => 1,
'Registrant ID' => 1,
'Registrant Name' => 1,
'Registrant Organization' => 1,
'Registrant Street' => 1,
'Registrant City' => 1,
'Registrant State/Province' => 1,
'Registrant Postal Code' => 1,
'Registrant Country' => 1,
'Registrant Phone' => 1,
'Registrant Phone Ext' => 1,
'Registrant Fax' => 1,
'Registrant Fax Ext' => 1,
'Registrant Email' => 1,
'Admin ID' => 1,
'Admin Name' => 1,
'Admin Organization' => 1,
'Admin Street' => 1,
'Admin City' => 1,
'Admin State/Province' => 1,
'Admin Postal Code' => 1,
'Admin Country' => 1,
'Admin Phone' => 1,
'Admin Phone Ext' => 1,
'Admin Fax' => 1,
'Admin Fax Ext' => 1,
'Admin Email' => 1,
'Tech ID' => 1,
'Tech Name' => 1,
'Tech Organization' => 1,
'Tech Street' => 1,
'Tech City' => 1,
'Tech State/Province' => 1,
'Tech Postal Code' => 1,
'Tech Country' => 1,
'Tech Phone' => 1,
'Tech Phone Ext' => 1,
'Tech Fax' => 1,
'Tech Fax Ext' => 1,
'Tech Email' => 1,
'DNSSEC' => 1,
'Name Server' => 1,
'IP Address' => 1,
);
Readonly my %REGISTRAR_ADDITIONAL_FIELD_KEY_BLACKLIST => (
'Registrar Name' => 1,
'Street' => 1,
'City' => 1,
'State/Province' => 1,
'Postal Code' => 1,
'Country' => 1,
'Phone Number' => 1,
'Email' => 1,
'WHOIS Server' => 1,
'Referral URL' => 1,
'Admin Contact' => 1,
'Technical Contact' => 1,
'Fax Number' => 1,
);
Readonly my %NAME_SERVER_ADDITIONAL_FIELD_KEY_BLACKLIST => (
'Server Name' => 1,
'IP Address' => 1,
'Registrar' => 1,
'WHOIS Server' => 1,
'Referral URL' => 1,
);
my $ROID_SUFFIX = {};
my %default_types;
......@@ -122,7 +206,7 @@ my %default_types;
return ( 'expected domain status code' );
}
if ( !exists $domain_status_codes{$value} ) {
if ( !exists $DOMAIN_STATUS_CODES{$value} ) {
return ( 'expected domain status code' );
}
......@@ -296,7 +380,7 @@ my %default_types;
}
if ( $value =~ /^([^ ]+) {1,9}https:\/\/icann\.org\/epp#(.+)$/o ) {
if ( exists $domain_status_codes{$1} && $1 eq $2 ) {
if ( exists $DOMAIN_STATUS_CODES{$1} && $1 eq $2 ) {
return ();
}
}
......@@ -401,6 +485,48 @@ my %default_types;
return ();
},
'domain name object additional field key' => sub {
my $value = shift;
unless ( $value ) {
return ( 'expected domain name object additional field key' );
}
if ( exists $DOMAIN_NAME_ADDITIONAL_FIELD_KEY_BLACKLIST{$value} ) {
return ( 'forbidden domain name object additional field key' );
}
return ();
},
'registrar object additional field key' => sub {
my $value = shift;
unless ( $value ) {
return ( 'expected registrar object additional field key' );
}
if ( exists $REGISTRAR_ADDITIONAL_FIELD_KEY_BLACKLIST{$value} ) {
return ( 'forbidden registrar object additional field key' );
}
return ();
},
'name server object additional field key' => sub {
my $value = shift;
unless ( $value ) {
return ( 'expected name server object additional field key' );
}
if ( exists $NAME_SERVER_ADDITIONAL_FIELD_KEY_BLACKLIST{$value} ) {
return ( 'forbidden name server object additional field key' );
}
return ();
},
'void' => sub {
return ( 'no values are allowed for type void' );
},
);
=head1 CONSTRUCTORS
......
......@@ -50,8 +50,8 @@ sub extract_roid {
}
elsif ( $token eq 'roid line' ) {
ref $value eq 'ARRAY' or croak "'roid line' value expected to be arrayref";
defined $value->[0] or croak "'roid line' value expected to have roid at position 0";
defined $value->[1] or croak "'hostname' value expected to have roid at position 1";
defined $value->[0] or croak "'roid line' value expected to have roid at position 0";
defined $value->[1] or croak "'hostname' value expected to have roid at position 1";
my ( $roid, $hostname ) = @{$value};
my @errors;
push @errors, grep { $_ ne 'expected roid suffix to be a registered epp repo id' } $types->validate_type( 'roid', $roid );
......
......@@ -95,13 +95,13 @@ sub validate {
};
# Validate rule
my $result = _rule( $state, key => $rule );
my ( $result, $rule_errors ) = _rule( $state, key => $rule, quantifier => 'required' );
# Pick up validation warnings
my @errors;
if ( defined $result ) {
ref $result eq 'ARRAY' or croak 'unexpected return value from _rule()';
@errors = @{$result};
ref $rule_errors eq 'ARRAY' or croak 'unexpected return value from _rule()';
@errors = @{$rule_errors};
}
# Check status of parsed input
......@@ -144,189 +144,278 @@ sub _describe_line {
}
}
sub _sequence_section {
my $state = shift or croak 'Missing argument: $state';
my $section_rule = shift or croak 'Missing argument: $section_rule';
=head2 B<_occurances( $state, key, line, type, quantifier, keytype )>
my @errors;
my $total = 0;
Parse a quantified grammar rule or a line type with the given $key.
for my $elem ( @$section_rule ) {
my $result = _occurances( $state, key => 'field', type => 'hostname', quantifier => 'required' );
ref $elem eq 'HASH' or confess;
Returns:
my ( $key, $params ) = %$elem;
=over 4
ref $params eq 'HASH' or confess "value of key '$key' must be a hashref";
=item B<()>
## no critic (TestingAndDebugging::ProhibitNoWarnings)
no warnings 'recursion';
## use critic
my ( $count, $result ) = _occurances( $state, %$params, key => $key );
No match. Input may have been consumed.
if ( !defined $count ) {
if ( $total == 0 ) {
return;
}
else {
my ( $token, $token_value, $token_errors ) = $state->{lexer}->peek_line();
defined $token or croak 'unexpected return value';
ref $token_errors eq 'ARRAY' or croak 'unexpected return value';
=item B<$result>
push @errors, @{$token_errors};
Match. Input may have been consumed.
my $description = _describe_line( $token, $token_value );
push @errors, sprintf( "line %d: %s not allowed here", $state->{lexer}->line_no, $description );
last;
}
}
ref $result eq 'ARRAY' or confess;
push @errors, @$result;
$total += $count;
}
B<$result> is an arrayref containing validation error strings.
return ( 'section', \@errors );
}
sub _choice_section {
my $