Skip to content

Commit

Permalink
feat: adds Specimen Molecular Analyte Type to sample metadata
Browse files Browse the repository at this point in the history
Co-authored-by: Clay McLeod <[email protected]>
  • Loading branch information
e-t-k and claymcleod committed Nov 15, 2024
1 parent f4dd6a1 commit da8fb60
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 3 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ Versioning](https://semver.org/spec/v2.0.0.html).
their responses
([discussion](https://github.com/CBIIT/ccdi-federation-api/discussions/79),
[#95](https://github.com/CBIIT/ccdi-federation-api/pull/95)).
- Adds Library Source Material ([link to
discussion](https://github.com/CBIIT/ccdi-federation-api/discussions/119),
[#118](https://github.com/CBIIT/ccdi-federation-api/pull/118))
- Adds Specimen Molecular Analyte Type ([link to
discussion](https://github.com/CBIIT/ccdi-federation-api/discussions/116),
[#123](https://github.com/CBIIT/ccdi-federation-api/pull/123),
[#122](https://github.com/CBIIT/ccdi-federation-api/issues/122))

### Revised

Expand Down
4 changes: 2 additions & 2 deletions packages/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"rust-analyzer.check.command": "clippy"
}
}
2 changes: 2 additions & 0 deletions packages/ccdi-cde/src/v1/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
mod disease_phase;
mod library_source_material;
mod library_strategy;
mod specimen_molecular_analyte_type;
mod tumor_classification;
mod tumor_tissue_morphology;

pub use disease_phase::DiseasePhase;
pub use library_source_material::LibrarySourceMaterial;
pub use library_strategy::LibraryStrategy;
pub use specimen_molecular_analyte_type::SpecimenMolecularAnalyteType;
pub use tumor_classification::TumorClassification;
pub use tumor_tissue_morphology::TumorTissueMorphology;
110 changes: 110 additions & 0 deletions packages/ccdi-cde/src/v1/sample/specimen_molecular_analyte_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use introspect::Introspect;
use rand::distributions::Standard;
use rand::prelude::Distribution;
use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;

use crate::CDE;

/// **`caDSR CDE 15063661 v1.00`**
///
/// This metadata element is defined by the caDSR as "The sample or material
/// being subjected to analysis.". This data element is a subset of the
/// designated CDE as noted
/// [here](https://github.com/CBIIT/ccdi-federation-api/discussions/116#discussioncomment-10848175).
///
/// Link:
/// <https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=15063661%20and%20ver_nr=1>
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema, Introspect)]
#[schema(as = cde::v1::sample::SpecimenMolecularAnalyteType)]
pub enum SpecimenMolecularAnalyteType {
/// `Protein`
///
/// * **VM Long Name**: Protein
/// * **VM Public ID**: 2581951
/// * **Concept Code**: C17021
/// * **Begin Date**: 11/15/2006
///
/// A group of complex organic macromolecules composed of one or more chains
/// (linear polymers) of alpha-L-amino acids linked by peptide bonds and
/// ranging in size from a few thousand to over 1 million Daltons. Proteins
/// are fundamental genetically encoded components of living cells with
/// specific structures and functions dictated by amino acid sequence.
#[serde(rename = "Protein")]
Protein,

/// `DNA`
///
/// * **VM Long Name**: DNA
/// * **VM Public ID**: 2581946
/// * **Concept Code**: C449
/// * **Begin Date**: 12/15/2015
///
/// A long linear double-stranded polymer formed from nucleotides attached
/// to a deoxyribose backbone and found in the nucleus of a cell; associated
/// with the transmission of genetic information.
#[serde(rename = "DNA")]
Dna,

/// `RNA`
///
/// * **VM Long Name**: RNA Specimen
/// * **VM Public ID**: 14239169
/// * **Concept Code**: C198568
/// * **Begin Date**: 08/01/2023
///
/// A biospecimen created to contain an isolated or enriched RNA sample.
#[serde(rename = "RNA")]
Rna,
}

impl CDE for SpecimenMolecularAnalyteType {}

impl std::fmt::Display for SpecimenMolecularAnalyteType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpecimenMolecularAnalyteType::Protein => write!(f, "Protein"),
SpecimenMolecularAnalyteType::Dna => write!(f, "DNA"),
SpecimenMolecularAnalyteType::Rna => write!(f, "RNA"),
}
}
}

impl Distribution<SpecimenMolecularAnalyteType> for Standard {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> SpecimenMolecularAnalyteType {
match rng.gen_range(0..=2) {
0 => SpecimenMolecularAnalyteType::Protein,
1 => SpecimenMolecularAnalyteType::Dna,
_ => SpecimenMolecularAnalyteType::Rna,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_converts_to_string_correctly() {
assert_eq!(SpecimenMolecularAnalyteType::Protein.to_string(), "Protein");
assert_eq!(SpecimenMolecularAnalyteType::Dna.to_string(), "DNA");
assert_eq!(SpecimenMolecularAnalyteType::Rna.to_string(), "RNA");
}

#[test]
fn it_serializes_to_json_correctly() {
assert_eq!(
serde_json::to_string(&SpecimenMolecularAnalyteType::Protein).unwrap(),
"\"Protein\""
);
assert_eq!(
serde_json::to_string(&SpecimenMolecularAnalyteType::Dna).unwrap(),
"\"DNA\""
);
assert_eq!(
serde_json::to_string(&SpecimenMolecularAnalyteType::Rna).unwrap(),
"\"RNA\""
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub fn get_field_descriptions() -> Vec<description::Description> {
cde::v1::sample::LibraryStrategy::description(),
cde::v1::sample::LibrarySourceMaterial::description(),
cde::v2::sample::PreservationMethod::description(),
cde::v1::sample::SpecimenMolecularAnalyteType::description(),
cde::v2::sample::TissueType::description(),
cde::v1::sample::TumorClassification::description(),
cde::v1::sample::TumorTissueMorphology::description(),
Expand Down Expand Up @@ -171,6 +172,28 @@ impl description::r#trait::Description for cde::v2::sample::PreservationMethod {
}
}

impl description::r#trait::Description for cde::v1::sample::SpecimenMolecularAnalyteType {
fn description() -> description::Description {
// SAFETY: these two unwraps are tested statically below in the test
// that constructs the description using `get_fields()`.
let entity = Self::entity().unwrap();
let members = Self::members().map(|member| member.unwrap());

description::Description::Harmonized(Harmonized::new(
Kind::Enum,
String::from("specimen_molecular_analyte_type"),
entity.description().to_string(),
"https://github.com/CBIIT/ccdi-federation-api/wiki/Sample-Metadata-Fields#specimen_molecular_analyte_type"
.parse::<Url>().unwrap(),
Some(Standard::new(
entity.standard_name().to_string(),
crate::Url::from(entity.standard_url().clone()),
)),
members,
))
}
}

impl description::r#trait::Description for cde::v2::sample::TissueType {
fn description() -> description::Description {
// SAFETY: these two unwraps are tested statically below in the test
Expand Down
9 changes: 9 additions & 0 deletions packages/ccdi-models/src/metadata/field/unowned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,15 @@ pub mod sample {
ccdi_cde as cde
);

unowned_field!(
SpecimenMolecularAnalyteType,
field::unowned::sample::SpecimenMolecularAnalyteType,
cde::v1::sample::SpecimenMolecularAnalyteType,
cde::v1::sample::SpecimenMolecularAnalyteType,
cde::v1::sample::SpecimenMolecularAnalyteType::Rna,
ccdi_cde as cde
);

unowned_field!(
Identifier,
field::unowned::sample::Identifier,
Expand Down
43 changes: 42 additions & 1 deletion packages/ccdi-models/src/sample/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ pub struct Metadata {
#[schema(value_type = field::unowned::sample::PreservationMethod, nullable = true)]
preservation_method: Option<field::unowned::sample::PreservationMethod>,

/// The sample or material being subjected to analysis.
#[schema(value_type = field::unowned::sample::SpecimenMolecularAnalyteType, nullable = true)]
specimen_molecular_analyte_type: Option<field::unowned::sample::SpecimenMolecularAnalyteType>,

/// The alternate identifiers for the sample.
///
/// Note that this list of identifiers *must* include the main identifier
Expand Down Expand Up @@ -325,6 +329,42 @@ impl Metadata {
self.preservation_method.as_ref()
}

/// Gets the harmonized specimen molecular analyte type for the [`Metadata`].
///
/// # Examples
///
/// ```
/// use ccdi_cde as cde;
/// use ccdi_models as models;
///
/// use models::metadata::field::unowned::sample::SpecimenMolecularAnalyteType;
/// use models::sample::metadata::Builder;
///
/// let metadata = Builder::default()
/// .specimen_molecular_analyte_type(SpecimenMolecularAnalyteType::new(
/// cde::v1::sample::SpecimenMolecularAnalyteType::Rna,
/// None,
/// None,
/// None,
/// ))
/// .build();
///
/// assert_eq!(
/// metadata.specimen_molecular_analyte_type(),
/// Some(&SpecimenMolecularAnalyteType::new(
/// cde::v1::sample::SpecimenMolecularAnalyteType::Rna,
/// None,
/// None,
/// None,
/// ))
/// );
/// ```
pub fn specimen_molecular_analyte_type(
&self,
) -> Option<&field::unowned::sample::SpecimenMolecularAnalyteType> {
self.specimen_molecular_analyte_type.as_ref()
}

/// Gets the harmonized tissue type for the [`Metadata`].
///
/// # Examples
Expand Down Expand Up @@ -655,6 +695,7 @@ impl Metadata {
library_strategy: rand::random(),
library_source_material: rand::random(),
preservation_method: rand::random(),
specimen_molecular_analyte_type: rand::random(),
tissue_type: rand::random(),
tumor_classification: rand::random(),
tumor_tissue_morphology: Some(field::unowned::sample::TumorTissueMorphology::new(
Expand Down Expand Up @@ -713,7 +754,7 @@ mod tests {
let metadata = builder::Builder::default().build();
assert_eq!(
&serde_json::to_string(&metadata).unwrap(),
"{\"age_at_diagnosis\":null,\"anatomical_sites\":null,\"diagnosis\":null,\"disease_phase\":null,\"tissue_type\":null,\"tumor_classification\":null,\"tumor_tissue_morphology\":null,\"age_at_collection\":null,\"library_strategy\":null,\"library_source_material\":null,\"preservation_method\":null,\"identifiers\":null,\"depositions\":null}"
"{\"age_at_diagnosis\":null,\"anatomical_sites\":null,\"diagnosis\":null,\"disease_phase\":null,\"tissue_type\":null,\"tumor_classification\":null,\"tumor_tissue_morphology\":null,\"age_at_collection\":null,\"library_strategy\":null,\"library_source_material\":null,\"preservation_method\":null,\"specimen_molecular_analyte_type\":null,\"identifiers\":null,\"depositions\":null}"
);
}
}
31 changes: 31 additions & 0 deletions packages/ccdi-models/src/sample/metadata/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub struct Builder {
/// The preservation method for this sample or biospecimen.
preservation_method: Option<field::unowned::sample::PreservationMethod>,

/// The specimen molecular analyte type for this sample.
specimen_molecular_analyte_type: Option<field::unowned::sample::SpecimenMolecularAnalyteType>,

/// The alternate identifiers for the sample.
identifiers: Option<Vec<field::unowned::sample::Identifier>>,

Expand Down Expand Up @@ -317,6 +320,33 @@ impl Builder {
self
}

/// Sets the `specimen_molecular_analyte_type` field of the [`Builder`].
///
/// # Examples
///
/// ```
/// use ccdi_cde as cde;
/// use ccdi_models as models;
///
/// use models::metadata::field::unowned::sample::SpecimenMolecularAnalyteType;
/// use models::sample::metadata::Builder;
///
/// let field = SpecimenMolecularAnalyteType::new(
/// cde::v1::sample::SpecimenMolecularAnalyteType::Rna,
/// None,
/// None,
/// None,
/// );
/// let builder = Builder::default().specimen_molecular_analyte_type(field);
/// ```
pub fn specimen_molecular_analyte_type(
mut self,
field: field::unowned::sample::SpecimenMolecularAnalyteType,
) -> Self {
self.specimen_molecular_analyte_type = Some(field);
self
}

/// Append a value to the `identifier` field of the [`Builder`].
///
/// # Examples
Expand Down Expand Up @@ -470,6 +500,7 @@ impl Builder {
library_strategy: self.library_strategy,
library_source_material: self.library_source_material,
preservation_method: self.preservation_method,
specimen_molecular_analyte_type: self.specimen_molecular_analyte_type,
tissue_type: self.tissue_type,
tumor_classification: self.tumor_classification,
tumor_tissue_morphology: self.tumor_tissue_morphology,
Expand Down
2 changes: 2 additions & 0 deletions packages/ccdi-openapi/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ use utoipa::openapi;
cde::v1::sample::LibraryStrategy,
cde::v1::sample::LibrarySourceMaterial,
cde::v2::sample::PreservationMethod,
cde::v1::sample::SpecimenMolecularAnalyteType,
cde::v2::sample::TissueType,
cde::v1::sample::TumorClassification,
cde::v1::sample::TumorTissueMorphology,
Expand Down Expand Up @@ -184,6 +185,7 @@ use utoipa::openapi;
field::unowned::sample::LibraryStrategy,
field::unowned::sample::LibrarySourceMaterial,
field::unowned::sample::PreservationMethod,
field::unowned::sample::SpecimenMolecularAnalyteType,
field::unowned::sample::TissueType,
field::unowned::sample::TumorClassification,
field::unowned::sample::TumorTissueMorphology,
Expand Down
7 changes: 7 additions & 0 deletions packages/ccdi-server/src/filter/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ impl FilterMetadataField<Sample, FilterSampleParams> for Vec<Sample> {
"library_strategy" => params.library_strategy.as_ref(),
"library_source_material" => params.library_source_material.as_ref(),
"preservation_method" => params.preservation_method.as_ref(),
"specimen_molecular_analyte_type" => params.specimen_molecular_analyte_type.as_ref(),
"tissue_type" => params.tissue_type.as_ref(),
"tumor_classification" => params.tumor_classification.as_ref(),
"age_at_diagnosis" => params.age_at_diagnosis.as_ref(),
Expand Down Expand Up @@ -60,6 +61,12 @@ impl FilterMetadataField<Sample, FilterSampleParams> for Vec<Sample> {
.metadata()
.and_then(|metadata| metadata.preservation_method())
.map(|preservation_method| vec![preservation_method.to_string()]),
"specimen_molecular_analyte_type" => sample
.metadata()
.and_then(|metadata| metadata.specimen_molecular_analyte_type())
.map(|specimen_molecular_analyte_type| {
vec![specimen_molecular_analyte_type.to_string()]
}),
"tissue_type" => sample
.metadata()
.and_then(|metadata| metadata.tissue_type())
Expand Down
6 changes: 6 additions & 0 deletions packages/ccdi-server/src/params/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ pub struct Sample {
#[param(required = false, nullable = false)]
pub preservation_method: Option<String>,

/// Matches any sample where the `specimen_molecular_analyte_type` field matches the string
/// provided.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[param(required = false, nullable = false)]
pub specimen_molecular_analyte_type: Option<String>,

/// Matches any sample where the `tissue_type` field matches the string
/// provided.
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down
14 changes: 14 additions & 0 deletions packages/ccdi-server/src/routes/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,20 @@ fn parse_field(field: &str, sample: &Sample) -> Option<Option<Value>> {
),
None => Some(None),
},
"specimen_molecular_analyte_type" => match sample.metadata() {
Some(metadata) => Some(
metadata
.specimen_molecular_analyte_type()
.as_ref()
// SAFETY: all metadata fields are able to be represented as
// [`serde_json::Value`]s.
.map(|specimen_molecular_analyte_type| {
serde_json::to_value(specimen_molecular_analyte_type.value()).unwrap()
})
.or(Some(Value::Null)),
),
None => Some(None),
},
"tissue_type" => match sample.metadata() {
Some(metadata) => Some(
metadata
Expand Down
Loading

0 comments on commit da8fb60

Please sign in to comment.