Skip to content

Commit

Permalink
feat: adds query parameter filtering to /sample and /subject
Browse files Browse the repository at this point in the history
  • Loading branch information
claymcleod committed Nov 17, 2023
1 parent b7b0571 commit e2a7154
Show file tree
Hide file tree
Showing 20 changed files with 699 additions and 84 deletions.
1 change: 1 addition & 0 deletions packages/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/ccdi-models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
#![feature(decl_macro)]
#![feature(trivial_bounds)]

/// A marker trait for queriable entities within this API.
pub trait Entity {}

pub mod metadata;
pub mod sample;
pub mod subject;
Expand Down
6 changes: 6 additions & 0 deletions packages/ccdi-models/src/metadata/field/owned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ macro_rules! owned_field {
$name::new(rand::random(), None, None, Some(false))
}
}

impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
};
}

Expand Down
6 changes: 6 additions & 0 deletions packages/ccdi-models/src/metadata/field/unowned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ macro_rules! unowned_field {
$name::new(rand::random(), None, None)
}
}

impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/ccdi-models/src/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub mod metadata;
pub use identifier::Identifier;
pub use metadata::Metadata;

use crate::Entity;

/// A sample.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[schema(as = models::Sample)]
Expand Down Expand Up @@ -128,6 +130,8 @@ impl Sample {
}
}

impl Entity for Sample {}

impl PartialOrd for Sample {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
Expand Down
18 changes: 9 additions & 9 deletions packages/ccdi-models/src/sample/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.disease_phase(),
/// &Some(DiseasePhase::new(
/// Some(&DiseasePhase::new(
/// cde::v1::sample::DiseasePhase::InitialDiagnosis,
/// None,
/// None
/// ))
/// );
/// ```
pub fn disease_phase(&self) -> &Option<field::unowned::sample::DiseasePhase> {
&self.disease_phase
pub fn disease_phase(&self) -> Option<&field::unowned::sample::DiseasePhase> {
self.disease_phase.as_ref()
}

/// Gets the harmonized tissue type for the [`Metadata`].
Expand All @@ -88,15 +88,15 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.tissue_type(),
/// &Some(TissueType::new(
/// Some(&TissueType::new(
/// cde::v2::sample::TissueType::Tumor,
/// None,
/// None
/// ))
/// );
/// ```
pub fn tissue_type(&self) -> &Option<field::unowned::sample::TissueType> {
&self.tissue_type
pub fn tissue_type(&self) -> Option<&field::unowned::sample::TissueType> {
self.tissue_type.as_ref()
}

/// Gets the harmonized tumor classification for the [`Metadata`].
Expand All @@ -120,15 +120,15 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.tumor_classification(),
/// &Some(TumorClassification::new(
/// Some(&TumorClassification::new(
/// cde::v1::sample::TumorClassification::Primary,
/// None,
/// None
/// ))
/// );
/// ```
pub fn tumor_classification(&self) -> &Option<field::unowned::sample::TumorClassification> {
&self.tumor_classification
pub fn tumor_classification(&self) -> Option<&field::unowned::sample::TumorClassification> {
self.tumor_classification.as_ref()
}

/// Gets the unharmonized fields for the [`Metadata`].
Expand Down
12 changes: 8 additions & 4 deletions packages/ccdi-models/src/subject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod metadata;
pub use kind::Kind;
pub use metadata::Metadata;

use crate::Entity;

/// A subject.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[schema(as = models::Subject)]
Expand Down Expand Up @@ -128,12 +130,12 @@ impl Subject {
/// Some(Builder::default().build()),
/// );
///
/// assert_eq!(subject.name(), &String::from("Name"));
/// assert_eq!(subject.name(), "Name");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn name(&self) -> &String {
&self.name
pub fn name(&self) -> &str {
self.name.as_str()
}

/// Gets the kind for this [`Subject`] by reference.
Expand Down Expand Up @@ -212,7 +214,7 @@ impl Subject {

Self {
id: identifier.clone(),
name: identifier.name().clone(),
name: identifier.name().to_string(),
kind: rand::random(),
metadata: match rng.gen_bool(0.7) {
true => Some(Metadata::random(identifier)),
Expand All @@ -222,6 +224,8 @@ impl Subject {
}
}

impl Entity for Subject {}

impl PartialOrd for Subject {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
Expand Down
24 changes: 12 additions & 12 deletions packages/ccdi-models/src/subject/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.sex(),
/// &Some(Sex::new(cde::v1::subject::Sex::Female, None, None))
/// Some(&Sex::new(cde::v1::subject::Sex::Female, None, None))
/// );
/// ```
pub fn sex(&self) -> &Option<field::unowned::subject::Sex> {
&self.sex
pub fn sex(&self) -> Option<&field::unowned::subject::Sex> {
self.sex.as_ref()
}

/// Gets the harmonized race(s) for the [`Metadata`].
Expand All @@ -84,11 +84,11 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.race(),
/// &Some(vec![Race::new(cde::v1::subject::Race::Asian, None, None)])
/// Some(&vec![Race::new(cde::v1::subject::Race::Asian, None, None)])
/// );
/// ```
pub fn race(&self) -> &Option<Vec<field::unowned::subject::Race>> {
&self.race
pub fn race(&self) -> Option<&Vec<field::unowned::subject::Race>> {
self.race.as_ref()
}

/// Gets the harmonized ethnicity for the [`Metadata`].
Expand All @@ -112,15 +112,15 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.ethnicity(),
/// &Some(Ethnicity::new(
/// Some(&Ethnicity::new(
/// cde::v2::subject::Ethnicity::NotHispanicOrLatino,
/// None,
/// None
/// ))
/// );
/// ```
pub fn ethnicity(&self) -> &Option<field::unowned::subject::Ethnicity> {
&self.ethnicity
pub fn ethnicity(&self) -> Option<&field::unowned::subject::Ethnicity> {
self.ethnicity.as_ref()
}

/// Gets the harmonized identifier(s) for the [`Metadata`].
Expand All @@ -145,16 +145,16 @@ impl Metadata {
///
/// assert_eq!(
/// metadata.identifiers(),
/// &Some(vec![Identifier::new(
/// Some(&vec![Identifier::new(
/// cde::v1::subject::Identifier::parse("organization:Name", ":").unwrap(),
/// None,
/// None,
/// Some(true)
/// )])
/// );
/// ```
pub fn identifiers(&self) -> &Option<Vec<field::owned::subject::Identifier>> {
&self.identifiers
pub fn identifiers(&self) -> Option<&Vec<field::owned::subject::Identifier>> {
self.identifiers.as_ref()
}

/// Gets the unharmonized fields for the [`Metadata`].
Expand Down
1 change: 1 addition & 0 deletions packages/ccdi-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ccdi-cde = { path = "../ccdi-cde" }
ccdi-models = { path = "../ccdi-models" }
indexmap.workspace = true
itertools = "0.11.0"
introspect.workspace = true
log.workspace = true
mime.workspace = true
rand.workspace = true
Expand Down
150 changes: 150 additions & 0 deletions packages/ccdi-server/src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Common filtering utilities.

use introspect::Introspected;

use ccdi_models as models;

use models::Entity;

pub mod sample;
pub mod subject;

/// A trait that defines a method for filtering by metadata values.
///
/// **Note:** can only be implemented for an API [`Entity`].
pub trait FilterMetadataField<T, P>
where
T: Entity,
{
/// Filters entities by checking if the value of the provided field name
/// matches the value of that field within the filter parameters. Matches
/// are case-sensitive.
fn filter_metadata_field(self, field: String, filter_params: &P) -> Vec<T>;
}

/// Filters a list of entities based on the provided filter parameters.
///
/// # Examples
///
/// ```
/// use ccdi_cde as cde;
/// use ccdi_models as models;
/// use ccdi_server as server;
///
/// use cde::v1::subject::Identifier;
/// use models::metadata::field::unowned::subject::Race;
/// use models::metadata::field::unowned::subject::Sex;
/// use models::subject::metadata::Builder;
/// use models::subject::Kind;
/// use models::Subject;
/// use server::filter::filter;
/// use server::params::filter::Subject as SubjectFilterParams;
///
/// let subjects = vec![
/// // A subject with no metadata.
/// Subject::new(
/// Identifier::new("organization", "SubjectName001"),
/// String::from("SubjectName001"),
/// Kind::Participant,
/// Some(Builder::default().build()),
/// ),
/// // A subject with metadata but no specified sex.
/// Subject::new(
/// Identifier::new("organization", "SubjectName002"),
/// String::from("SubjectName002"),
/// Kind::Participant,
/// Some(Builder::default().build()),
/// ),
/// // A subject with sex 'F'.
/// Subject::new(
/// Identifier::new("organization", "SubjectName003"),
/// String::from("SubjectName003"),
/// Kind::Participant,
/// Some(
/// Builder::default()
/// .sex(Sex::new(cde::v1::subject::Sex::Female, None, None))
/// .build(),
/// ),
/// ),
/// // A subject with sex 'F' and race 'Asian'.
/// Subject::new(
/// Identifier::new("organization", "SubjectName004"),
/// String::from("SubjectName004"),
/// Kind::Participant,
/// Some(
/// Builder::default()
/// .sex(Sex::new(cde::v1::subject::Sex::Female, None, None))
/// .append_race(Race::new(cde::v1::subject::Race::Asian, None, None))
/// .build(),
/// ),
/// ),
/// ];
///
/// // Filtering of subjects with no parameters.
/// let mut results =
/// filter::<Subject, SubjectFilterParams>(subjects.clone(), SubjectFilterParams::default());
///
/// assert_eq!(results.len(), 4);
///
/// // Filtering of subjects with "F" in sex field.
/// let mut results = filter::<Subject, SubjectFilterParams>(
/// subjects.clone(),
/// SubjectFilterParams {
/// sex: Some(String::from("F")),
/// race: None,
/// ethnicity: None,
/// identifiers: None,
/// },
/// );
///
/// assert_eq!(results.len(), 2);
/// assert_eq!(results.first().unwrap().name(), "SubjectName003");
/// assert_eq!(results.last().unwrap().name(), "SubjectName004");
///
/// // Filtering of subjects with "F" in sex field and "Asi" in race field.
/// let mut results = filter::<Subject, SubjectFilterParams>(
/// subjects.clone(),
/// SubjectFilterParams {
/// sex: Some(String::from("F")),
/// race: Some(String::from("Asian")),
/// ethnicity: None,
/// identifiers: None,
/// },
/// );
///
/// assert_eq!(results.len(), 1);
/// assert_eq!(results.pop().unwrap().name(), "SubjectName004");
///
/// // Filtering of subjects is case-sensitive.
/// let mut results = filter::<Subject, SubjectFilterParams>(
/// subjects.clone(),
/// SubjectFilterParams {
/// sex: Some(String::from("f")),
/// race: None,
/// ethnicity: None,
/// identifiers: None,
/// },
/// );
///
/// assert_eq!(results.len(), 0);
/// ```
pub fn filter<T, P>(mut entities: Vec<T>, filter_params: P) -> Vec<T>
where
T: Entity,
Vec<T>: FilterMetadataField<T, P>,
P: Introspected,
{
for member in P::introspected_members() {
let field = match member {
// SAFETY: parameters will _always_ be expression as a struct with
// named fields. If they are not, this method will not work.
introspect::Member::Field(field) => field.identifier().unwrap().to_string(),
// SAFETY: parameters will never be expressed as an `enum`.
introspect::Member::Variant(_) => unreachable!(),
};

entities = entities.filter_metadata_field(field, &filter_params);
}

entities
}
Loading

0 comments on commit e2a7154

Please sign in to comment.