Skip to content

Commit

Permalink
feat: adds pagination and formalized errors
Browse files Browse the repository at this point in the history
* Pagination support was added to the `/sample` and `/subject`
  endpoints. This includes many details on supported query parameters,
  response headers, etc. Please see the rules outlined in the Swagger
  specification for more details.
* Formalized errors were introduced into the API in this commit. In
  short, there are now three error types: `InvalidParameter`,
  `NotFound`, and `UnsupportedField`. These each have defined error
  messages that **must** be returned by all servers.
* Moved the total counts mechanism out of the `/sample` and `/subject`
  endpoints and into the new `/sample/summary` and `/subject/summary`
  endpoints. This means that `/sample` and `/subject` can now return a
  Not Found (`404`) response code if the server does not support sharing
  line-level data. See #26 for more details.
  • Loading branch information
claymcleod committed Nov 5, 2023
1 parent 001b783 commit c166e90
Show file tree
Hide file tree
Showing 31 changed files with 2,169 additions and 204 deletions.
246 changes: 242 additions & 4 deletions packages/Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ regex = "1.10.2"
serde = { version = "1.0.189", features = ["serde_derive"] }
serde_json = { version = "1.0.107", features = ["preserve_order"] }
serde_test = "1.0.176"
serde_with = "3.4.0"
utoipa = { version = "4.0.0", features = [
"actix_extras",
"indexmap",
"preserve_order",
"preserve_path_order",
Expand Down
4 changes: 3 additions & 1 deletion packages/ccdi-cde/src/v1/subject/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ impl std::error::Error for Error {}
///
/// Link:
/// <https://cadsr.cancer.gov/onedata/dmdirect/NIH/NCI/CO/CDEDD?filter=CDEDD.ITEM_ID=6380049%20and%20ver_nr=1>
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema, Introspect)]
#[derive(
Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, ToSchema, Introspect,
)]
#[schema(as = cde::v1::subject::Identifier)]
pub struct Identifier {
/// The namespace of the identifier.
Expand Down
5 changes: 0 additions & 5 deletions packages/ccdi-models/src/count.rs

This file was deleted.

Empty file.
19 changes: 0 additions & 19 deletions packages/ccdi-models/src/count/total.rs

This file was deleted.

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

pub mod count;
pub mod metadata;
pub mod sample;
pub mod subject;
Expand Down
63 changes: 62 additions & 1 deletion packages/ccdi-models/src/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub use identifier::Identifier;
pub use metadata::Metadata;

/// A sample.
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[schema(as = models::Sample)]
pub struct Sample {
/// The identifier for this [`Sample`].
Expand Down Expand Up @@ -127,3 +127,64 @@ impl Sample {
}
}
}

impl PartialOrd for Sample {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

// Samples are sorted purely by identifier: the values contained _within_ a
// [`Sample`] are not relevant to the sort order. They are, however, relevant
// to equality—thus, why [`Eq`] and [`PartialEq`] are derived.
impl Ord for Sample {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}

#[cfg(test)]
mod tests {
use std::cmp::Ordering;

use super::*;

#[test]
fn it_orders_samples_correctly() {
let a = Sample::new(Identifier::parse("organization:A", ":").unwrap(), None);
let b = Sample::new(Identifier::parse("organization:B", ":").unwrap(), None);

assert_eq!(a.cmp(&b), Ordering::Less);

let c = Sample::new(Identifier::parse("organization:C", ":").unwrap(), None);
let b = Sample::new(Identifier::parse("organization:B", ":").unwrap(), None);

assert_eq!(c.cmp(&b), Ordering::Greater);

let foo = Sample::new(Identifier::parse("organization:Name", ":").unwrap(), None);
let bar = Sample::new(Identifier::parse("organization:Name", ":").unwrap(), None);

assert_eq!(foo.cmp(&bar), Ordering::Equal);
}

#[test]
fn it_tests_equality_correctly() {
let foo = Sample::new(Identifier::parse("organization:B", ":").unwrap(), None);
let bar = Sample::new(Identifier::parse("organization:B", ":").unwrap(), None);

assert!(foo == bar);

let foo = Sample::new(Identifier::parse("organization:A", ":").unwrap(), None);
let bar = Sample::new(Identifier::parse("organization:B", ":").unwrap(), None);

assert!(foo != bar);

let foo = Sample::new(
Identifier::parse("organization:Name", ":").unwrap(),
Some(metadata::Builder::default().build()),
);
let bar = Sample::new(Identifier::parse("organization:Name", ":").unwrap(), None);

assert!(foo != bar);
}
}
2 changes: 1 addition & 1 deletion packages/ccdi-models/src/sample/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl std::fmt::Display for Error {
impl std::error::Error for Error {}

/// The primary name and namespace for a sample used within the source server.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, ToSchema)]
#[schema(as = models::sample::Identifier)]
pub struct Identifier {
/// The namespace of the identifier.
Expand Down
120 changes: 119 additions & 1 deletion packages/ccdi-models/src/subject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub use kind::Kind;
pub use metadata::Metadata;

/// A subject.
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[schema(as = models::Subject)]
pub struct Subject {
/// The primary identifier used by the site.
Expand Down Expand Up @@ -221,3 +221,121 @@ impl Subject {
}
}
}

impl PartialOrd for Subject {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

// Subjects are sorted purely by identifier: the values contained _within_ a
// [`Subject`] are not relevant to the sort order. They are, however, relevant
// to equality—thus, why [`Eq`] and [`PartialEq`] are derived.
impl Ord for Subject {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}

#[cfg(test)]
mod tests {
use std::cmp::Ordering;

use super::*;

#[test]
fn it_orders_samples_correctly() {
let a = Subject::new(
Identifier::parse("organization:A", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);
let b = Subject::new(
Identifier::parse("organization:B", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);

assert_eq!(a.cmp(&b), Ordering::Less);

let c = Subject::new(
Identifier::parse("organization:C", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);
let b = Subject::new(
Identifier::parse("organization:B", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);

assert_eq!(c.cmp(&b), Ordering::Greater);

let foo = Subject::new(
Identifier::parse("organization:Name", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);
let bar = Subject::new(
Identifier::parse("organization:Name", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);

assert_eq!(foo.cmp(&bar), Ordering::Equal);
}

#[test]
fn it_tests_equality_correctly() {
let foo = Subject::new(
Identifier::parse("organization:Name", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);
let bar = Subject::new(
Identifier::parse("organization:Name", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);

assert!(foo == bar);

let foo = Subject::new(
Identifier::parse("organization:A", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);
let bar = Subject::new(
Identifier::parse("organization:B", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);

assert!(foo != bar);

let foo = Subject::new(
Identifier::parse("organization:Name", ":").unwrap(),
String::from("Name"),
Kind::Participant,
None,
);
let bar = Subject::new(
Identifier::parse("organization:Name", ":").unwrap(),
String::from("Name"),
Kind::Participant,
Some(metadata::Builder::default().build()),
);

assert!(foo != bar);
}
}
17 changes: 12 additions & 5 deletions packages/ccdi-openapi/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ a variety of query parameters.",
server::routes::subject::subject_index,
server::routes::subject::subject_show,
server::routes::subject::subjects_by_count,
server::routes::subject::subject_summary,
// Sample routes.
server::routes::sample::sample_index,
server::routes::sample::sample_show,
server::routes::sample::samples_by_count,
server::routes::sample::sample_summary,
// Metadata.
server::routes::metadata::metadata_fields_subject,
Expand Down Expand Up @@ -120,11 +122,12 @@ a variety of query parameters.",
models::metadata::field::description::Harmonized,
models::metadata::field::description::Unharmonized,
// Counts.
models::count::Total,
// General responses.
responses::Error,
responses::Errors,
// Summary responses.
responses::summary::Counts,
responses::Summary,
// Subject responses.
responses::Subject,
Expand All @@ -137,7 +140,11 @@ a variety of query parameters.",
responses::by::count::Samples,
// Metadata responses.
responses::metadata::FieldDescriptions
responses::metadata::FieldDescriptions,
// Error responses.
responses::error::Kind,
responses::Errors
)),
modifiers(&RemoveLicense)
)]
Expand Down
3 changes: 3 additions & 0 deletions packages/ccdi-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ actix-web.workspace = true
ccdi-cde = { path = "../ccdi-cde" }
ccdi-models = { path = "../ccdi-models" }
indexmap.workspace = true
itertools = "0.11.0"
log.workspace = true
mime.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
url = { version = "2", features = ["serde"] }
utoipa.workspace = true
2 changes: 2 additions & 0 deletions packages/ccdi-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
#![warn(missing_debug_implementations)]
#![deny(rustdoc::broken_intra_doc_links)]

pub mod paginate;
pub mod params;
pub mod responses;
pub mod routes;
Loading

0 comments on commit c166e90

Please sign in to comment.