Convert-Pheno

 view release on metacpan or  search on metacpan

lib/Convert/Pheno/BFF/ToPXF.pm  view on Meta::CPAN

    # ========
    # diseases
    # ========
    _map_diseases( $self, $bff, $pxf );

    # ===============
    # medicalActions
    # ===============
    _map_medical_actions( $self, $bff, $pxf );

    # =================================================
    # Phenopacket payload preserved under info.phenopacket
    # =================================================
    _map_phenopacket_payload( $self, $bff, $pxf );

    # =====
    # files
    # =====
    # (Not mapped, see original code comments)

    # =========
    # metaData
    # =========
    _map_metaData( $self, $bff, $pxf );

# =========
# exposures
# =========

# Phenopackets v2 does not define an "exposures" field in the Phenopacket schema.
#
# Sept 2023 -> pxf-tools error:
#   Message type "org.phenopackets.schema.v2.Phenopacket" has no field named "exposures".
#   Available fields: ['id', 'subject', 'phenotypicFeatures', 'measurements', 'biosamples',
#                      'interpretations', 'diseases', 'medicalActions', 'files', 'metaData']
#
# Explanation provided by @gsfk (issue #4): Exposure was proposed for v2 but not
# adopted and later removed (ga4gh/phenopacket-schema@cb8bf58). Some documentation
# still incorrectly lists it.
#
# Do not map exposures; this block is legacy.
# Tracking: https://github.com/CNAG-Biomedical-Informatics/convert-pheno/issues/4
# Observed Sept 2023; reconfirmed Feb 2026

#   $pxf->{exposures} =
#
#      [
#        map {
#            {
#                type       => $_->{exposureCode},
#                occurrence => { timestamp => $_->{date} }
#            }
#        } @{ $bff->{exposures} }
#      ]
#      if exists $bff->{exposures};

    #######################################
    # END MAPPING TO PHENOPACKET V2 TERMS #
    #######################################

    _strip_private_keys($pxf);

    return $pxf;
}

sub _map_id {
    my ( $self, $bff, $pxf ) = @_;

    # ==
    # id
    # ==
    $pxf->{id} =
      $self->{test}
      ? undef
      : 'phenopacket_id.' . generate_random_alphanumeric_string(8);
}

sub _map_subject {
    my ( $self, $bff, $pxf ) = @_;

    my $vitalStatus = _resolve_vitalStatus( $self, $bff );

    # =======
    # subject
    # =======
    $pxf->{subject} = {
        id          => $bff->{id},
        vitalStatus => $vitalStatus,
    };

    my $sex = _map_sex( $bff->{sex} );
    $pxf->{subject}{sex} = $sex if defined $sex;

    my $dateOfBirth = _get_phenopacket_info_term( $bff, 'dateOfBirth' );
    $pxf->{subject}{dateOfBirth} = $dateOfBirth if defined $dateOfBirth;

    # karyotypicSex
    $pxf->{subject}{karyotypicSex} = $bff->{karyotypicSex}
      if exists $bff->{karyotypicSex};
}

sub _map_phenotypic_features {
    my ( $self, $bff, $pxf ) = @_;

 # ===================
 # phenotypicFeatures
 # ===================
 # Assign transformed 'phenotypicFeatures' to $pxf, only if it's defined in $bff
    $pxf->{phenotypicFeatures} = [
        map {
            my $feature = _clone_data($_);
            my %mapped;

            $mapped{type} = _clone_data( $feature->{featureType} )
              if exists $feature->{featureType};
            $mapped{excluded} =
              exists $feature->{excluded}
              ? $feature->{excluded}
              : JSON::PP::false();
            $mapped{severity} = _clone_data( $feature->{severity} )
              if exists $feature->{severity};

lib/Convert/Pheno/BFF/ToPXF.pm  view on Meta::CPAN

    my ( $self, $bff, $pxf ) = @_;

    # =========
    # metaData
    # =========
    # Depending on the origion (redcap) , _info and resources may exist
    $pxf->{metaData} =
        $self->{test} ? undef
      : defined( _get_phenopacket_info_term( $bff, 'metaData' ) )
      ? _clone_data( _get_phenopacket_info_term( $bff, 'metaData' ) )
      : get_metaData($self);
}

#----------------------------------------------------------------------
# Helper subs
#----------------------------------------------------------------------

sub _get_phenopacket_info_term {
    my ( $bff, $term ) = @_;

    return unless exists $bff->{info} && ref( $bff->{info} ) eq 'HASH';

    return $bff->{info}{phenopacket}{$term}
      if exists $bff->{info}{phenopacket}
      && ref( $bff->{info}{phenopacket} ) eq 'HASH'
      && exists $bff->{info}{phenopacket}{$term};

    return $bff->{info}{$term} if exists $bff->{info}{$term};

    return;
}

sub _resolve_vitalStatus {
    my ( $self, $bff ) = @_;

    my $vitalStatus = _get_phenopacket_info_term( $bff, 'vitalStatus' );
    return _clone_data($vitalStatus) if defined $vitalStatus;

    return { status => $self->{default_vital_status} || 'ALIVE' };
}

sub _clone_data {
    my ($data) = @_;
    return undef unless defined $data;

    if ( ref($data) eq 'HASH' ) {
        return { map { $_ => _clone_data( $data->{$_} ) } keys %{$data} };
    }

    if ( ref($data) eq 'ARRAY' ) {
        return [ map { _clone_data($_) } @{$data} ];
    }

    if ( blessed($data) && blessed($data) eq 'JSON::PP::Boolean' ) {
        return $data ? JSON::PP::true() : JSON::PP::false();
    }

    return $data;
}

sub _strip_private_keys {
    my ($data) = @_;
    return unless defined $data;

    if ( ref($data) eq 'HASH' ) {
        for my $key ( keys %{$data} ) {
            if ( $key =~ /^_/ ) {
                delete $data->{$key};
                next;
            }
            _strip_private_keys( $data->{$key} );
        }
        return;
    }

    if ( ref($data) eq 'ARRAY' ) {
        _strip_private_keys($_) for @{$data};
        return;
    }

    return;
}

sub _map_sex {
    my ($sex) = @_;
    return 'UNKNOWN_SEX' unless defined $sex;

    my $id    = ref($sex) eq 'HASH' ? $sex->{id}    : undef;
    my $label = ref($sex) eq 'HASH' ? $sex->{label} : $sex;

    return 'MALE'   if defined $id && $id eq 'NCIT:C20197';
    return 'FEMALE' if defined $id && $id eq 'NCIT:C16576';

    return 'MALE'        if defined $label && $label =~ /^male$/i;
    return 'FEMALE'      if defined $label && $label =~ /^female$/i;
    return 'OTHER_SEX'   if defined $label && $label =~ /^other(?:_sex)?$/i;
    return 'UNKNOWN_SEX' if defined $label
      && $label =~ /^(?:unknown(?:[_ ]sex)?|not available)$/i;

    return 'UNKNOWN_SEX';
}

sub _looks_like_pxf_time_element {
    my ($time) = @_;
    return 0 unless ref($time) eq 'HASH';

    return scalar grep { exists $time->{$_} }
      qw(age ageRange gestationalAge interval ontologyClass timestamp);
}

sub _map_time_element_to_pxf {
    my ($time) = @_;
    return unless defined $time;

    return _clone_data($time) if _looks_like_pxf_time_element($time);

    if ( !ref($time) ) {
        return {
            timestamp => $time =~ /^\d{4}-\d{2}-\d{2}$/
            ? map_iso8601_date2timestamp($time)
            : $time,
        };
    }

    return _clone_data($time) unless ref($time) eq 'HASH';

    if ( exists $time->{iso8601duration} ) {
        return { age => _clone_data($time) };
    }

    if ( exists $time->{weeks} || exists $time->{days} ) {
        return { gestationalAge => _clone_data($time) };
    }

    if ( exists $time->{id} ) {
        return { ontologyClass => _clone_data($time) };
    }



( run in 1.229 second using v1.01-cache-2.11-cpan-13bb782fe5a )