From 5344a3725645f94a841108f573b610758b950509 Mon Sep 17 00:00:00 2001 From: Emma Baghurst <156218556+em-baggie@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:35:51 +0000 Subject: [PATCH 1/2] Add json config --- .../src/config/snomed_usage_data_urls.json | 15 ++ rust/codelist-builder-rs/src/errors.rs | 12 ++ rust/codelist-builder-rs/src/lib.rs | 1 - .../src/snomed_usage_data.rs | 150 +++--------------- rust/codelist-builder-rs/src/usage_year.rs | 100 ------------ .../tests/download_usage.rs | 29 ++-- 6 files changed, 59 insertions(+), 248 deletions(-) create mode 100644 rust/codelist-builder-rs/src/config/snomed_usage_data_urls.json delete mode 100644 rust/codelist-builder-rs/src/usage_year.rs diff --git a/rust/codelist-builder-rs/src/config/snomed_usage_data_urls.json b/rust/codelist-builder-rs/src/config/snomed_usage_data_urls.json new file mode 100644 index 0000000..739fc86 --- /dev/null +++ b/rust/codelist-builder-rs/src/config/snomed_usage_data_urls.json @@ -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" +} \ No newline at end of file diff --git a/rust/codelist-builder-rs/src/errors.rs b/rust/codelist-builder-rs/src/errors.rs index b5ec0a8..81bc60d 100644 --- a/rust/codelist-builder-rs/src/errors.rs +++ b/rust/codelist-builder-rs/src/errors.rs @@ -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 { @@ -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), diff --git a/rust/codelist-builder-rs/src/lib.rs b/rust/codelist-builder-rs/src/lib.rs index 9da729b..ac08411 100644 --- a/rust/codelist-builder-rs/src/lib.rs +++ b/rust/codelist-builder-rs/src/lib.rs @@ -1,3 +1,2 @@ pub mod errors; pub mod snomed_usage_data; -pub mod usage_year; diff --git a/rust/codelist-builder-rs/src/snomed_usage_data.rs b/rust/codelist-builder-rs/src/snomed_usage_data.rs index 82d30f4..611f9aa 100644 --- a/rust/codelist-builder-rs/src/snomed_usage_data.rs +++ b/rust/codelist-builder-rs/src/snomed_usage_data.rs @@ -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 /// @@ -60,29 +62,34 @@ pub struct SnomedUsageDataEntry { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct SnomedUsageData { pub usage_data: Vec, - 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, ) -> Result { - 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_data_urls.json").to_string(), + }; + + let config: HashMap = 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(); @@ -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 @@ -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 @@ -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(()) - } } diff --git a/rust/codelist-builder-rs/src/usage_year.rs b/rust/codelist-builder-rs/src/usage_year.rs deleted file mode 100644 index aed3985..0000000 --- a/rust/codelist-builder-rs/src/usage_year.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! This file contains the usage year enum and its implementation - -// Internal imports -use crate::errors::CodeListBuilderError; - -// External imports -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -/// Enum to represent usage year -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum UsageYear { - Y2011_12, - Y2012_13, - Y2013_14, - Y2014_15, - Y2015_16, - Y2016_17, - Y2017_18, - Y2018_19, - Y2019_20, - Y2020_21, - Y2021_22, - Y2022_23, - Y2023_24, -} - -impl FromStr for UsageYear { - type Err = CodeListBuilderError; - - /// Convert a string to a usage year - /// - /// # Arguments - /// * `s` - The string to convert - /// - /// # Returns Self or an error if the string is not a valid usage year - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "2011-12" => Ok(UsageYear::Y2011_12), - "2012-13" => Ok(UsageYear::Y2012_13), - "2013-14" => Ok(UsageYear::Y2013_14), - "2014-15" => Ok(UsageYear::Y2014_15), - "2015-16" => Ok(UsageYear::Y2015_16), - "2016-17" => Ok(UsageYear::Y2016_17), - "2017-18" => Ok(UsageYear::Y2017_18), - "2018-19" => Ok(UsageYear::Y2018_19), - "2019-20" => Ok(UsageYear::Y2019_20), - "2020-21" => Ok(UsageYear::Y2020_21), - "2021-22" => Ok(UsageYear::Y2021_22), - "2022-23" => Ok(UsageYear::Y2022_23), - "2023-24" => Ok(UsageYear::Y2023_24), - invalid_string => Err(CodeListBuilderError::invalid_usage_year(invalid_string)), - } - } -} - -impl UsageYear { - /// Get the URL for the usage year - /// - /// # Returns - /// * `String` - The path for the usage year - pub fn path(&self) -> String { - match self { - UsageYear::Y2011_12 => "/53/C8F877/SNOMED_code_usage_2011-12.txt".to_string(), - UsageYear::Y2012_13 => "/69/866A44/SNOMED_code_usage_2012-13.txt".to_string(), - UsageYear::Y2013_14 => "/82/40F702/SNOMED_code_usage_2013-14.txt".to_string(), - UsageYear::Y2014_15 => "/BB/47E566/SNOMED_code_usage_2014-15.txt".to_string(), - UsageYear::Y2015_16 => "/8B/15EAA1/SNOMED_code_usage_2015-16.txt".to_string(), - UsageYear::Y2016_17 => "/E2/79561E/SNOMED_code_usage_2016-17.txt".to_string(), - UsageYear::Y2017_18 => "/9F/024949/SNOMED_code_usage_2017-18.txt".to_string(), - UsageYear::Y2018_19 => "/13/F2956B/SNOMED_code_usage_2018-19.txt".to_string(), - UsageYear::Y2019_20 => "/8F/882EB3/SNOMED_code_usage_2019-20.txt".to_string(), - UsageYear::Y2020_21 => "/8A/09BBE6/SNOMED_code_usage_2020-21.txt".to_string(), - UsageYear::Y2021_22 => "/71/6C02F5/SNOMED_code_usage_2021-22.txt".to_string(), - UsageYear::Y2022_23 => "/09/E1218D/SNOMED_code_usage_2022-23.txt".to_string(), - UsageYear::Y2023_24 => "/B8/7D8335/SNOMED_code_usage_2023-24.txt".to_string(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - #[test] - fn test_from_str() -> Result<(), CodeListBuilderError> { - let usage_year = UsageYear::from_str("2011-12")?; - assert_eq!(usage_year, UsageYear::Y2011_12); - Ok(()) - } - - #[test] - fn test_path() { - let usage_year = UsageYear::Y2015_16; - let url = usage_year.path(); - let expected_url = "/8B/15EAA1/SNOMED_code_usage_2015-16.txt".to_string(); - assert_eq!(url, expected_url); - } -} diff --git a/rust/codelist-builder-rs/tests/download_usage.rs b/rust/codelist-builder-rs/tests/download_usage.rs index b178e09..fec99e0 100644 --- a/rust/codelist-builder-rs/tests/download_usage.rs +++ b/rust/codelist-builder-rs/tests/download_usage.rs @@ -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()); From 02a89821954dc1ce95d884e092033de1fc298cc4 Mon Sep 17 00:00:00 2001 From: Emma Baghurst <156218556+em-baggie@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:39:03 +0000 Subject: [PATCH 2/2] Rename config file --- .../{snomed_usage_data_urls.json => snomed_usage_config.json} | 0 rust/codelist-builder-rs/src/snomed_usage_data.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename rust/codelist-builder-rs/src/config/{snomed_usage_data_urls.json => snomed_usage_config.json} (100%) diff --git a/rust/codelist-builder-rs/src/config/snomed_usage_data_urls.json b/rust/codelist-builder-rs/src/config/snomed_usage_config.json similarity index 100% rename from rust/codelist-builder-rs/src/config/snomed_usage_data_urls.json rename to rust/codelist-builder-rs/src/config/snomed_usage_config.json diff --git a/rust/codelist-builder-rs/src/snomed_usage_data.rs b/rust/codelist-builder-rs/src/snomed_usage_data.rs index 611f9aa..6acf5a3 100644 --- a/rust/codelist-builder-rs/src/snomed_usage_data.rs +++ b/rust/codelist-builder-rs/src/snomed_usage_data.rs @@ -80,7 +80,7 @@ impl SnomedUsageData { ) -> Result { let config_string = match config_path { Some(path) => fs::read_to_string(path)?, - None => include_str!("config/snomed_usage_data_urls.json").to_string(), + None => include_str!("config/snomed_usage_config.json").to_string(), }; let config: HashMap = serde_json::from_str(&config_string)?;