From da8fb60c7b6699f3067a5fe7a1913d86f101489e Mon Sep 17 00:00:00 2001 From: e-t-k Date: Fri, 15 Nov 2024 14:57:41 -0600 Subject: [PATCH] feat: adds Specimen Molecular Analyte Type to sample metadata Co-authored-by: Clay McLeod --- CHANGELOG.md | 7 ++ packages/.vscode/settings.json | 4 +- packages/ccdi-cde/src/v1/sample.rs | 2 + .../sample/specimen_molecular_analyte_type.rs | 110 ++++++++++++++++++ .../field/description/harmonized/sample.rs | 23 ++++ .../ccdi-models/src/metadata/field/unowned.rs | 9 ++ packages/ccdi-models/src/sample/metadata.rs | 43 ++++++- .../src/sample/metadata/builder.rs | 31 +++++ packages/ccdi-openapi/src/api.rs | 2 + packages/ccdi-server/src/filter/sample.rs | 7 ++ packages/ccdi-server/src/params/filter.rs | 6 + packages/ccdi-server/src/routes/sample.rs | 14 +++ swagger.yml | 51 ++++++++ 13 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 packages/ccdi-cde/src/v1/sample/specimen_molecular_analyte_type.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b923ef1..970219d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/.vscode/settings.json b/packages/.vscode/settings.json index 1570300..9ae8ae3 100644 --- a/packages/.vscode/settings.json +++ b/packages/.vscode/settings.json @@ -1,4 +1,4 @@ { - "editor.formatOnSave": true, + "editor.formatOnSave": false, "rust-analyzer.check.command": "clippy" -} \ No newline at end of file +} diff --git a/packages/ccdi-cde/src/v1/sample.rs b/packages/ccdi-cde/src/v1/sample.rs index 1ac3275..9ccc9ea 100644 --- a/packages/ccdi-cde/src/v1/sample.rs +++ b/packages/ccdi-cde/src/v1/sample.rs @@ -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; diff --git a/packages/ccdi-cde/src/v1/sample/specimen_molecular_analyte_type.rs b/packages/ccdi-cde/src/v1/sample/specimen_molecular_analyte_type.rs new file mode 100644 index 0000000..9b9d176 --- /dev/null +++ b/packages/ccdi-cde/src/v1/sample/specimen_molecular_analyte_type.rs @@ -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: +/// +#[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 for Standard { + fn sample(&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\"" + ); + } +} diff --git a/packages/ccdi-models/src/metadata/field/description/harmonized/sample.rs b/packages/ccdi-models/src/metadata/field/description/harmonized/sample.rs index 3db8e5e..d2599d9 100644 --- a/packages/ccdi-models/src/metadata/field/description/harmonized/sample.rs +++ b/packages/ccdi-models/src/metadata/field/description/harmonized/sample.rs @@ -23,6 +23,7 @@ pub fn get_field_descriptions() -> Vec { 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(), @@ -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::().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 diff --git a/packages/ccdi-models/src/metadata/field/unowned.rs b/packages/ccdi-models/src/metadata/field/unowned.rs index ee29c77..e8aaa12 100644 --- a/packages/ccdi-models/src/metadata/field/unowned.rs +++ b/packages/ccdi-models/src/metadata/field/unowned.rs @@ -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, diff --git a/packages/ccdi-models/src/sample/metadata.rs b/packages/ccdi-models/src/sample/metadata.rs index 78587d8..99d6aa9 100644 --- a/packages/ccdi-models/src/sample/metadata.rs +++ b/packages/ccdi-models/src/sample/metadata.rs @@ -77,6 +77,10 @@ pub struct Metadata { #[schema(value_type = field::unowned::sample::PreservationMethod, nullable = true)] preservation_method: Option, + /// The sample or material being subjected to analysis. + #[schema(value_type = field::unowned::sample::SpecimenMolecularAnalyteType, nullable = true)] + specimen_molecular_analyte_type: Option, + /// The alternate identifiers for the sample. /// /// Note that this list of identifiers *must* include the main identifier @@ -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 @@ -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( @@ -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}" ); } } diff --git a/packages/ccdi-models/src/sample/metadata/builder.rs b/packages/ccdi-models/src/sample/metadata/builder.rs index e554bb1..73e004e 100644 --- a/packages/ccdi-models/src/sample/metadata/builder.rs +++ b/packages/ccdi-models/src/sample/metadata/builder.rs @@ -42,6 +42,9 @@ pub struct Builder { /// The preservation method for this sample or biospecimen. preservation_method: Option, + /// The specimen molecular analyte type for this sample. + specimen_molecular_analyte_type: Option, + /// The alternate identifiers for the sample. identifiers: Option>, @@ -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 @@ -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, diff --git a/packages/ccdi-openapi/src/api.rs b/packages/ccdi-openapi/src/api.rs index 565d55e..e4f888a 100644 --- a/packages/ccdi-openapi/src/api.rs +++ b/packages/ccdi-openapi/src/api.rs @@ -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, @@ -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, diff --git a/packages/ccdi-server/src/filter/sample.rs b/packages/ccdi-server/src/filter/sample.rs index 246da05..4faa56e 100644 --- a/packages/ccdi-server/src/filter/sample.rs +++ b/packages/ccdi-server/src/filter/sample.rs @@ -16,6 +16,7 @@ impl FilterMetadataField for Vec { "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(), @@ -60,6 +61,12 @@ impl FilterMetadataField for Vec { .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()) diff --git a/packages/ccdi-server/src/params/filter.rs b/packages/ccdi-server/src/params/filter.rs index a6ae99f..c1d37ac 100644 --- a/packages/ccdi-server/src/params/filter.rs +++ b/packages/ccdi-server/src/params/filter.rs @@ -111,6 +111,12 @@ pub struct Sample { #[param(required = false, nullable = false)] pub preservation_method: Option, + /// 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, + /// Matches any sample where the `tissue_type` field matches the string /// provided. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/packages/ccdi-server/src/routes/sample.rs b/packages/ccdi-server/src/routes/sample.rs index 55c86ea..a978488 100644 --- a/packages/ccdi-server/src/routes/sample.rs +++ b/packages/ccdi-server/src/routes/sample.rs @@ -471,6 +471,20 @@ fn parse_field(field: &str, sample: &Sample) -> Option> { ), 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 diff --git a/swagger.yml b/swagger.yml index d9f5d59..1d615fe 100644 --- a/swagger.yml +++ b/swagger.yml @@ -385,6 +385,14 @@ paths: required: false schema: type: string + - name: specimen_molecular_analyte_type + in: query + description: |- + Matches any sample where the `specimen_molecular_analyte_type` field matches the string + provided. + required: false + schema: + type: string - name: tissue_type in: query description: |- @@ -1412,6 +1420,22 @@ components: - WGA - WGS - WXS + cde.v1.sample.SpecimenMolecularAnalyteType: + type: string + description: |- + **`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: + + enum: + - Protein + - DNA + - RNA cde.v1.sample.TumorClassification: type: string description: |- @@ -2030,6 +2054,28 @@ components: comment: type: string description: A free-text comment field. + field.unowned.sample.SpecimenMolecularAnalyteType: + type: object + required: + - value + properties: + value: + $ref: '#/components/schemas/cde.v1.sample.SpecimenMolecularAnalyteType' + ancestors: + type: array + items: + type: string + description: |- + The ancestors from which this field was derived. + + Ancestors should be provided as period (`.`) delimited paths + from the `metadata` key in the subject response object. + details: + allOf: + - $ref: '#/components/schemas/models.metadata.field.Details' + comment: + type: string + description: A free-text comment field. field.unowned.sample.TissueType: type: object required: @@ -3205,6 +3251,7 @@ components: - library_strategy - library_source_material - preservation_method + - specimen_molecular_analyte_type - identifiers properties: age_at_diagnosis: @@ -3251,6 +3298,10 @@ components: allOf: - $ref: '#/components/schemas/field.unowned.sample.PreservationMethod' nullable: true + specimen_molecular_analyte_type: + allOf: + - $ref: '#/components/schemas/field.unowned.sample.SpecimenMolecularAnalyteType' + nullable: true identifiers: type: array items: