Skip to content

Commit

Permalink
feat: adds /sample endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
claymcleod committed Oct 23, 2023
1 parent ad0b4ac commit bb1ac9b
Show file tree
Hide file tree
Showing 54 changed files with 4,069 additions and 437 deletions.
47 changes: 41 additions & 6 deletions packages/Cargo.lock

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

2 changes: 2 additions & 0 deletions packages/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ edition = "2021"
[workspace.dependencies]
actix-web = "4.4.0"
indexmap = "2.0.2"
introspect = { git = "https://github.com/claymcleod/introspect.git" }
log = "0.4.20"
mime = "0.3.17"
rand = "0.8.5"
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"
Expand Down
3 changes: 3 additions & 0 deletions packages/ccdi-cde/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ license.workspace = true
edition.workspace = true

[dependencies]
indexmap.workspace = true
introspect.workspace = true
rand.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
utoipa.workspace = true
131 changes: 121 additions & 10 deletions packages/ccdi-cde/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,133 @@
#![warn(rust_2021_compatibility)]
#![warn(missing_debug_implementations)]
#![deny(rustdoc::broken_intra_doc_links)]
#![feature(trivial_bounds)]

use introspect::Entity;
use introspect::Introspected;
use introspect::Member;

use crate::parse::cde::member;

pub mod parse;
pub mod v1;
pub mod v2;

/// A trait to define the harmonization standard used for a common data element.
pub trait Standard {
/// Gets the harmonization standard name for a common data element.
fn standard() -> &'static str;
/// An error related to a [`CDE`].
#[derive(Debug)]
pub enum Error {
/// The common data element is missing documentation.
MissingDocumentation,

/// An error occurred while parsing an entity.
EntityError(parse::cde::entity::ParseError),

/// An error occurred while parsing a member of an entity.
MemberError(parse::cde::member::ParseError),
}

/// A trait to define the URL where users can learn more about a common data
/// element.
pub trait Url {
/// Gets the URL to learn more about a common data element.
fn url() -> &'static str;
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::MissingDocumentation => write!(f, "missing documentation"),
Error::EntityError(err) => write!(f, "parse entity error: {err}"),
Error::MemberError(err) => write!(f, "parse member error: {err}"),
}
}
}

impl std::error::Error for Error {}

/// A [`Result`](std::result::Result) with an [`Error`].
pub type Result<T> = std::result::Result<T, Error>;

/// A marker trait for common data elements (CDEs).
pub trait CDE: std::fmt::Display + Eq + PartialEq + Standard + Url {}
pub trait CDE: std::fmt::Display + Eq + PartialEq + Introspected {
/// Gets the parsed entity information from the corresponding entity's
/// documentation.
fn entity() -> Result<parse::cde::Entity> {
let entity = Self::introspected_entity();

let documentation = match &entity {
Entity::Enum(entity) => entity.documentation().to_owned(),
Entity::Struct(entity) => entity.documentation().to_owned(),
}
.map(Ok)
.unwrap_or(Err(Error::MissingDocumentation))?;

documentation
.parse::<parse::cde::Entity>()
.map_err(Error::EntityError)
}

/// Gets the parsed members of an entity from the corresponding member's
/// documentation.
fn members() -> Result<Vec<(String, parse::cde::Member)>> {
Self::introspected_members()
.into_iter()
.map(|member| match member {
Member::Field(member) => {
member
.documentation()
.map(|doc| match doc.parse::<member::Field>() {
Ok(field) => Ok((
member.identifier().unwrap_or("<unnamed>").to_string(),
crate::parse::cde::Member::Field(field),
)),
Err(err) => {
Err(Error::MemberError(member::ParseError::FieldError(err)))
}
})
}
Member::Variant(member) => member.documentation().map(|doc| match doc
.parse::<member::Variant>()
{
Ok(variant) => Ok((
member.identifier().to_string(),
crate::parse::cde::Member::Variant(variant),
)),
Err(err) => Err(Error::MemberError(member::ParseError::VariantError(err))),
}),
})
.map(|member| member.unwrap_or(Err(Error::MissingDocumentation)))
.collect::<Result<Vec<_>>>()
}
}

#[cfg(test)]
mod tests {
use crate::v1::subject::Sex;

use super::*;

#[test]
fn entity_parsing_works_correctly() {
let entity = Sex::entity().unwrap();

assert_eq!(entity.standard(), "caDSR CDE 6343385 v1.00");
}

#[test]
fn member_parsing_works_correctly() {
let mut entity = Sex::members().unwrap().into_iter();

let (identifer, variant) = entity.next().unwrap();
assert_eq!(identifer, "Unknown");
assert_eq!(variant.get_variant().unwrap().permissible_value(), "U");

let (identifer, variant) = entity.next().unwrap();
assert_eq!(identifer, "Female");
assert_eq!(variant.get_variant().unwrap().permissible_value(), "F");

let (identifer, variant) = entity.next().unwrap();
assert_eq!(identifer, "Male");
assert_eq!(variant.get_variant().unwrap().permissible_value(), "M");

let (identifer, variant) = entity.next().unwrap();
assert_eq!(identifer, "Undifferentiated");
assert_eq!(
variant.get_variant().unwrap().permissible_value(),
"UNDIFFERENTIATED"
);
}
}
71 changes: 71 additions & 0 deletions packages/ccdi-cde/src/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Parsing common data elements and their members from documentation.

use std::iter::Peekable;
use std::str::Lines;

/// Conventional line endings in text files (platform-specific).
#[cfg(windows)]
pub const LINE_ENDING: &str = "\r\n";

/// Conventional line endings in text files (platform-specific).
#[cfg(not(windows))]
pub const LINE_ENDING: &str = "\n";

pub mod cde;

/// Reads from an iterator over lines and trims the concatenates lines together.
/// This is useful when you want to treat a multiline comment as a single
/// [`String`].
///
/// Note that, if the contiguous lines are postceded by an empty line, then the
/// postceding empty line is also consumed. This is convenient for calling this
/// function multiple times in a row on the same iterator.
///
/// # Examples
///
/// ```
/// use ccdi_cde as cde;
///
/// use cde::parse::trim_and_concat_contiguous_lines;
///
/// let mut lines = "hello\nthere,\nworld\n\nfoo\nbar\n\n\"baz\ntest\"".lines().peekable();
///
/// assert_eq!(
/// trim_and_concat_contiguous_lines(&mut lines),
/// Some(String::from("hello there, world"))
/// );
///
/// assert_eq!(
/// trim_and_concat_contiguous_lines(&mut lines),
/// Some(String::from("foo bar"))
/// );
///
/// assert_eq!(
/// trim_and_concat_contiguous_lines(&mut lines),
/// Some(String::from(r#""baz test""#))
/// );
///
/// assert_eq!(
/// trim_and_concat_contiguous_lines(&mut lines),
/// None
/// );
/// ```
pub fn trim_and_concat_contiguous_lines(lines: &mut Peekable<Lines<'_>>) -> Option<String> {
// If the first line is `None`, return `None`;
lines.peek()?;

let mut results = vec![];

for line in lines.by_ref() {
let line = line.trim();

// Consume the postceding empty line.
if line.is_empty() {
break;
}

results.push(line);
}

Some(results.join(" ").trim().to_string())
}
7 changes: 7 additions & 0 deletions packages/ccdi-cde/src/parse/cde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Parsing information for common data elements.

pub mod entity;
pub mod member;

pub use entity::Entity;
pub use member::Member;
Loading

0 comments on commit bb1ac9b

Please sign in to comment.