package LIMS::Local::GenomicsValidation;
use Moo;
use Local::Utils;
use Clone qw(clone);
use Data::Printer alias => 'p';
use XML::SAX::ParserFactory;
use XML::Validator::Schema;
has xsd_src => ( is => 'ro', required => 1 );
has dbix => ( is => 'ro', required => 1 );
has cfg => ( is => 'ro', required => 1 );
has rare_disease_base => ( is => 'lazy' );
has family_id_prefix => ( is => 'lazy' );
has cancer_increment => ( is => 'lazy' );
has lab_tests_map => ( is => 'lazy' );
sub _build_cancer_increment { shift->cfg->{genomics}->{uat}->{cancer_incr} }
sub _build_rare_disease_base { shift->cfg->{genomics}->{uat}->{rare_disease} }
sub _build_family_id_prefix {
my $self = shift;
# family_id_prefix = 1st 3 digits of rare_disease participant id base:
my ($family_id_prefix) = $self->rare_disease_base =~ /^(\d{3})/; # p $family_id_prefix;
return $family_id_prefix;
}
sub _build_lab_tests_map { # warn '=' x 30;
my $self = shift;
my $sql = q!SELECT lt.test_name, lt.field_label, ls.section_name
FROM lab_tests lt JOIN lab_sections ls on lt.lab_section_id = ls.id!;
$self->dbix->query($sql)->hash_map('test_name');
}
no autovivification;
sub validate_params {
my ($self, $data) = @_; # p $data; # hashref
my %errors;
my $c = clone $data; # to prevent auto-vivification
# family ID (is RD-specific) is 9 digits & begins with 1st 3 digits of
# rare_disease participant id base (122xxxxxx):
if ( my $family_id = $c->{Approach}->{family_id} ) { # warn $family_id;
=begin # family_id should be participant_id + max 9 individuals, but doesn't
# work in practice as family_id just has to match a participant_id which
# could be any number of requests different
my $participant_id = $c->{Demographics}->{participant_id};
unless ( $participant_id - $family_id ~~ [0..9] ) {
# p [ $participant_id, $family_id ];
$h{family_id} = $family_id;
}
=cut
my $err_msg;
# family_id_prefix is 1st 3 digits of rare_disease participant id base:
my $family_id_prefix = $self->family_id_prefix; # p $family_id_prefix;
# must be rare-disease type:
my $arm = $c->{Demographics}->{arm}; # p $arm;
if ( ! $arm || lc $arm ne 'rare disease' ) {
$err_msg = 'family ID only valid for rare disease category';
}
# must begin with rare-disease prefix (122) + 6 digits:
elsif ( $family_id !~ /^$family_id_prefix\d{6}$/ ) {
$err_msg = 'invalid format (require 122xxxxxx)';
}
else {
# check family_id belongs to an existing participant_id (unit_number):
my $pid_match_ok = $self->dbix->select('patient_case', 1,
{ unit_number => $family_id })->value; # p $pid_match_ok; p $family_id;
unless ( $pid_match_ok ) {
$err_msg = 'no matching proband';
}
}
if ($err_msg) {
my %h = ( val => $family_id, err => $err_msg );
$errors{family_id} = \%h;
}
}
# participant ID is 9 digits and matches allocated RD or Cancer block:
if ( my $participant_id = $c->{Demographics}->{participant_id} ) { # p $participant_id;
my $request_id = $c->{Demographics}->{request_id};
my $expected = $self->rare_disease_base + $request_id;
if ( my $arm = $c->{Demographics}->{arm} ) {
$expected += $self->cancer_increment if lc $arm eq 'cancer';
} # p $expected;
else { # not screened:
$expected = 0; # ensures failure even if length = 9
}
my $match_ok = _matches($participant_id, $expected);
unless ( $match_ok && length $participant_id == 9 ) {
my $err = $expected # non-value means request unscreened
? $match_ok # must be invalid length if match_ok
? 'does not match expected length (9 characters)'
: 'does not match expected numerical range for disease type'
: 'unexpected value for unscreened record';
my %h = (
val => $participant_id,
err => $err,
);
$errors{participant_id} = \%h;
# need to add participant_id (ie unit number) manually to
# $self->lab_test_map for error decode for notification:
$self->lab_tests_map->{participant_id} ||= { # unless already added
section_name => 'Patient demographics', # not strictly a lab-section
field_label => 'Unit number',
}; # p $map;
}
}
# nhs number:
unless ( $c->{Demographics}->{nhs_number} ) {
$errors{nhs_number} = undef;
}
=begin # validated in validate_xml_against_xsd() using XML libs now
# DNA/volume_stored is xs:small (integer):
if ( my $val = $data->{DNA}->{volume_stored} ) {
unless ($val =~ /^\d+$/) { # warn 'here';
$data->{DNA}->{volume_stored} = int($val);
}
}
# DNA/edta1_fluidx_vol is xs:small (integer):
if ( my $val = $data->{DNA}->{edta1_fluidx_vol} ) {
unless ($val =~ /^\d+$/) { # warn 'here';
$data->{DNA}->{edta1_fluidx_vol} = int($val);
}
}
=cut
{ # sample ID's (9-digits, decimal, 1/2-digits:
my %data = ();
{ # Specimens - blood section:
$data{$_} = $c->{SpecimensBlood}->{$_} for
qw( paxgene_rna edta1 edta2 sst pst );
}
{ # Specimens - saliva section:
$data{$_} = $c->{SpecimensSaliva}->{$_} for
qw( oragene_tube_1 oragene_tube_2 );
}
# tumour specimen ID:
$data{sample_id} = $c->{SpecimensTumour}->{sample_id}; # p \%data;
# bone marrow specimen ID:
$data{bm_sample_id} = $c->{SpecimensBoneMarrow}->{bm_sample_id};
while ( my ($test, $val) = each %data ) { # warn $test; # warn $val;
next unless $val && $val !~ /^\d{9}\.\d{1,2}$/;
$errors{$test} = { val => $val, err => undef }; # use default err msg
}
}
# p \%errors;
my $lab_tests_map = $self->lab_tests_map; # p $lab_tests_map;
my $default_error = 'does not match required definition';
my @errs = map { # same format as genomics_xml.pl notify_admin msg:
( $errors{$_}
? ( sprintf qq!Illegal value '%s' !, $errors{$_}{val} )
: 'Illegal NULL value ' )
. sprintf q!for '%s' in '%s' section, %s.!,
$lab_tests_map->{$_}{field_label},
$lab_tests_map->{$_}{section_name},
$errors{$_}{err} || $default_error;
} keys %errors;
return @errs ? \@errs : 0; # don't return empty arrayref (evals 'true' for caller)
}
#-------------------------------------------------------------------------------
sub validate_xml_against_xsd {
my ($self, $xml) = @_; # warn $xml;
my $xsd_src = $self->xsd_src;
# switch debug on to show data structure:
my $validator = XML::Validator::Schema->new(file => $xsd_src, debug => 0); # p $validator;
my $parser = XML::SAX::ParserFactory->parser(Handler => $validator);
eval { $parser->parse_string($xml); };
return $@;
}
#-------------------------------------------------------------------------------
sub reformat_error {
my ($self, $error) = @_;
my ($lab_test) = $error =~ m!element <(.*)>!; # p $lab_test;
my $lab_tests_map = $self->lab_tests_map; # p $lab_tests_map;
if ( my $ref = $lab_tests_map->{$lab_test} ) { # get field label & section
my $section = $ref->{section_name};
my $label = $ref->{field_label};
$error =~ s/in element <$lab_test>/for '$label' in '$section' section/; # p $error;
}
return $error;
}
sub _matches { Local::Utils::matches(@_) }
1;