Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions rust/codelist-builder-rs/src/config/snomed_usage_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"2011-12": "https://files.digital.nhs.uk/53/C8F877/SNOMED_code_usage_2011-12.txt",
"2012-13": "https://files.digital.nhs.uk/69/866A44/SNOMED_code_usage_2012-13.txt",
"2013-14": "https://files.digital.nhs.uk/82/40F702/SNOMED_code_usage_2013-14.txt",
"2014-15": "https://files.digital.nhs.uk/BB/47E566/SNOMED_code_usage_2014-15.txt",
"2015-16": "https://files.digital.nhs.uk/8B/15EAA1/SNOMED_code_usage_2015-16.txt",
"2016-17": "https://files.digital.nhs.uk/E2/79561E/SNOMED_code_usage_2016-17.txt",
"2017-18": "https://files.digital.nhs.uk/9F/024949/SNOMED_code_usage_2017-18.txt",
"2018-19": "https://files.digital.nhs.uk/13/F2956B/SNOMED_code_usage_2018-19.txt",
"2019-20": "https://files.digital.nhs.uk/8F/882EB3/SNOMED_code_usage_2019-20.txt",
"2020-21": "https://files.digital.nhs.uk/8A/09BBE6/SNOMED_code_usage_2020-21.txt",
"2021-22": "https://files.digital.nhs.uk/71/6C02F5/SNOMED_code_usage_2021-22.txt",
"2022-23": "https://files.digital.nhs.uk/09/E1218D/SNOMED_code_usage_2022-23.txt",
"2023-24": "https://files.digital.nhs.uk/B8/7D8335/SNOMED_code_usage_2023-24.txt"
}
12 changes: 12 additions & 0 deletions rust/codelist-builder-rs/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/// Enum to represent the different types of errors that can occur in the
/// codelist-builder library
use std::io;

#[derive(Debug, thiserror::Error, thiserror_ext::Construct)]
pub enum CodeListBuilderError {
Expand All @@ -14,10 +15,21 @@ pub enum CodeListBuilderError {
#[error("HTTP error code: {code}: {body}")]
HttpErrorCode { code: String, body: String },

#[error("URL not found in config for year {year}")]
UrlNotFound { year: String },

#[error("HTTP request error: {0}")]
#[construct(skip)]
ReqwestError(#[from] reqwest::Error),

#[error("IO error: {0}")]
#[construct(skip)]
IOError(#[from] io::Error),

#[error("JSON error: {0}")]
#[construct(skip)]
JSONError(#[from] serde_json::Error),

#[error("CSV error: {0}")]
#[construct(skip)]
CSVError(#[from] csv::Error),
Expand Down
1 change: 0 additions & 1 deletion rust/codelist-builder-rs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pub mod errors;
pub mod snomed_usage_data;
pub mod usage_year;
150 changes: 19 additions & 131 deletions rust/codelist-builder-rs/src/snomed_usage_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@
//! 1 = SNOMED concept was published and was active.
//! 0 = SNOMED concept was either not yet available or was inactive.

use std::fs;

// Internal imports
use crate::errors::CodeListBuilderError;
use crate::usage_year::UsageYear;

// External imports
use csv;
use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Struct to represent a snomed usage data entry
///
Expand All @@ -60,29 +62,34 @@ pub struct SnomedUsageDataEntry {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SnomedUsageData {
pub usage_data: Vec<SnomedUsageDataEntry>,
pub usage_year: UsageYear,
pub usage_year: String,
}

impl SnomedUsageData {
/// Download snomed usage data from a url
///
/// # Arguments
/// * `base_url` - The base url
/// * `usage_year` - The usage year
/// * `config_path` - Optional path to config file
///
/// # Returns
/// Self or an error if the download fails
pub async fn download_usage(
base_url: &str,
usage_year: UsageYear,
usage_year: &str,
config_path: Option<String>,
) -> Result<Self, CodeListBuilderError> {
let url = format!(
"{}/{}",
base_url.trim_end_matches('/'),
usage_year.path().trim_start_matches('/')
);
let config_string = match config_path {
Some(path) => fs::read_to_string(path)?,
None => include_str!("config/snomed_usage_config.json").to_string(),
};

let config: HashMap<String, String> = serde_json::from_str(&config_string)?;

let response = reqwest::get(&url).await.map_err(CodeListBuilderError::from)?;
let url = config
.get(usage_year)
.ok_or_else(|| CodeListBuilderError::url_not_found(usage_year))?;

let response = reqwest::get(url).await?;
if !response.status().is_success() {
let status = response.status().to_string();
let body = response.text().await.unwrap_or_default();
Expand All @@ -92,7 +99,7 @@ impl SnomedUsageData {

let usage_data = Self::parse_from_string(&body)?;

Ok(SnomedUsageData { usage_data, usage_year })
Ok(SnomedUsageData { usage_data, usage_year: usage_year.to_string() })
}

/// Parse snomed usage data from a string
Expand Down Expand Up @@ -151,8 +158,6 @@ impl SnomedUsageData {
mod tests {
use super::*;
use crate::errors::CodeListBuilderError;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

const LONG_TEST_DATA: &str = "SNOMED_Concept_ID Description Usage Active_at_Start Active_at_End
279991000000102 Short message service text message sent to patient (procedure) 122292090 1 1
Expand Down Expand Up @@ -323,121 +328,4 @@ mod tests {
assert!(error_string.contains("CSV error:"));
Ok(())
}

#[tokio::test]
async fn test_download_usage_from_url() -> Result<(), CodeListBuilderError> {
let mock_server = MockServer::start().await;
let usage_year = UsageYear::Y2020_21;

let test_data = LONG_TEST_DATA;

Mock::given(method("GET"))
.and(path(usage_year.path()))
.respond_with(ResponseTemplate::new(200).set_body_string(test_data))
.mount(&mock_server)
.await;

let result = SnomedUsageData::download_usage(&mock_server.uri(), usage_year).await?;

let usage_data = result.usage_data;
let usage_year = result.usage_year;

assert_eq!(usage_data.len(), 10);

assert_eq!(usage_data[0].snomed_concept_id, "279991000000102");
assert_eq!(
usage_data[0].description,
"Short message service text message sent to patient (procedure)"
);
assert_eq!(usage_data[0].usage, "122292090");
assert!(usage_data[0].active_at_start);
assert!(usage_data[0].active_at_end);

assert_eq!(usage_data[1].snomed_concept_id, "163030003");
assert_eq!(
usage_data[1].description,
"On examination - Systolic blood pressure reading (finding)"
);
assert_eq!(usage_data[1].usage, "59227180");
assert!(usage_data[1].active_at_start);
assert!(usage_data[1].active_at_end);

assert_eq!(usage_data[2].snomed_concept_id, "163031004");
assert_eq!(
usage_data[2].description,
"On examination - Diastolic blood pressure reading (finding)"
);
assert_eq!(usage_data[2].usage, "59184050");
assert!(usage_data[2].active_at_start);
assert!(usage_data[2].active_at_end);

assert_eq!(usage_data[3].snomed_concept_id, "163020007");
assert_eq!(usage_data[3].description, "On examination - blood pressure reading (finding)");
assert_eq!(usage_data[3].usage, "37837700");
assert!(usage_data[3].active_at_start);
assert!(usage_data[3].active_at_end);

assert_eq!(usage_data[4].snomed_concept_id, "1000731000000107");
assert_eq!(usage_data[4].description, "Serum creatinine level (observable entity)");
assert_eq!(usage_data[4].usage, "33211250");
assert!(usage_data[4].active_at_start);
assert!(usage_data[4].active_at_end);

assert_eq!(usage_data[5].snomed_concept_id, "1000661000000107");
assert_eq!(usage_data[5].description, "Serum sodium level (observable entity)");
assert_eq!(usage_data[5].usage, "31630420");
assert!(usage_data[5].active_at_start);
assert!(usage_data[5].active_at_end);

assert_eq!(usage_data[6].snomed_concept_id, "1000651000000109");
assert_eq!(usage_data[6].description, "Serum potassium level (observable entity)");
assert_eq!(usage_data[6].usage, "31542470");
assert!(usage_data[6].active_at_start);
assert!(usage_data[6].active_at_end);

assert_eq!(usage_data[7].snomed_concept_id, "162763007");
assert_eq!(usage_data[7].description, "On examination - weight (finding)");
assert_eq!(usage_data[7].usage, "30836800");
assert!(usage_data[7].active_at_start);
assert!(usage_data[7].active_at_end);

assert_eq!(usage_data[8].snomed_concept_id, "1022431000000105");
assert_eq!(usage_data[8].description, "Haemoglobin estimation (observable entity)");
assert_eq!(usage_data[8].usage, "29864410");
assert!(usage_data[8].active_at_start);
assert!(usage_data[8].active_at_end);

assert_eq!(usage_data[9].snomed_concept_id, "4468401000001106");
assert_eq!(
usage_data[9].description,
"Triptorelin 3.75mg injection (pdr for recon)+solvent prefilled syringe (product)"
);
assert_eq!(usage_data[9].usage, "80");
assert!(!usage_data[9].active_at_start);
assert!(!usage_data[9].active_at_end);

assert_eq!(usage_year, UsageYear::Y2020_21);

Ok(())
}

#[tokio::test]
async fn test_download_usage_from_url_error_response() -> Result<(), CodeListBuilderError> {
let mock_server = MockServer::start().await;
let usage_year = UsageYear::Y2020_21;

Mock::given(method("GET"))
.and(path(usage_year.path()))
.respond_with(ResponseTemplate::new(500))
.mount(&mock_server)
.await;

let error =
SnomedUsageData::download_usage(&mock_server.uri(), usage_year).await.unwrap_err();
let error_string = error.to_string();

assert_eq!(&error_string, "HTTP error code: 500 Internal Server Error: ");

Ok(())
}
}
100 changes: 0 additions & 100 deletions rust/codelist-builder-rs/src/usage_year.rs

This file was deleted.

29 changes: 13 additions & 16 deletions rust/codelist-builder-rs/tests/download_usage.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
use codelist_builder_rs::errors::CodeListBuilderError;
use codelist_builder_rs::snomed_usage_data::SnomedUsageData;
use codelist_builder_rs::usage_year::UsageYear;

#[tokio::test]
async fn test_download_usage() -> Result<(), CodeListBuilderError> {
let base_url = "https://files.digital.nhs.uk";

let result_2011_12 = SnomedUsageData::download_usage(base_url, UsageYear::Y2011_12).await?;
let result_2012_13 = SnomedUsageData::download_usage(base_url, UsageYear::Y2012_13).await?;
let result_2013_14 = SnomedUsageData::download_usage(base_url, UsageYear::Y2013_14).await?;
let result_2014_15 = SnomedUsageData::download_usage(base_url, UsageYear::Y2014_15).await?;
let result_2015_16 = SnomedUsageData::download_usage(base_url, UsageYear::Y2015_16).await?;
let result_2016_17 = SnomedUsageData::download_usage(base_url, UsageYear::Y2016_17).await?;
let result_2017_18 = SnomedUsageData::download_usage(base_url, UsageYear::Y2017_18).await?;
let result_2018_19 = SnomedUsageData::download_usage(base_url, UsageYear::Y2018_19).await?;
let result_2019_20 = SnomedUsageData::download_usage(base_url, UsageYear::Y2019_20).await?;
let result_2020_21 = SnomedUsageData::download_usage(base_url, UsageYear::Y2020_21).await?;
let result_2021_22 = SnomedUsageData::download_usage(base_url, UsageYear::Y2021_22).await?;
let result_2022_23 = SnomedUsageData::download_usage(base_url, UsageYear::Y2022_23).await?;
let result_2023_24 = SnomedUsageData::download_usage(base_url, UsageYear::Y2023_24).await?;
let result_2011_12 = SnomedUsageData::download_usage("2011-12", None).await?;
let result_2012_13 = SnomedUsageData::download_usage("2012-13", None).await?;
let result_2013_14 = SnomedUsageData::download_usage("2013-14", None).await?;
let result_2014_15 = SnomedUsageData::download_usage("2014-15", None).await?;
let result_2015_16 = SnomedUsageData::download_usage("2015-16", None).await?;
let result_2016_17 = SnomedUsageData::download_usage("2016-17", None).await?;
let result_2017_18 = SnomedUsageData::download_usage("2017-18", None).await?;
let result_2018_19 = SnomedUsageData::download_usage("2018-19", None).await?;
let result_2019_20 = SnomedUsageData::download_usage("2019-20", None).await?;
let result_2020_21 = SnomedUsageData::download_usage("2020-21", None).await?;
let result_2021_22 = SnomedUsageData::download_usage("2021-22", None).await?;
let result_2022_23 = SnomedUsageData::download_usage("2022-23", None).await?;
let result_2023_24 = SnomedUsageData::download_usage("2023-24", None).await?;

assert!(!result_2011_12.usage_data.is_empty());
assert!(!result_2012_13.usage_data.is_empty());
Expand Down
Loading