Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/run_preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ jobs:
- name: Clippy check
working-directory: rust
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Unused dependency check
uses: bnjbvr/cargo-machete@main
2 changes: 1 addition & 1 deletion .github/workflows/run_python_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
python-version: ['3.11', '3.12', '3.13', '3.14']

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[workspace]
resolver = "3"
members = [
"rust/codelist-rs",
"rust/codelist-systems-rs",
"rust/codelist-validator-rs",
"rust/codelist-builder-rs",
"bindings/python",
Expand Down
23 changes: 18 additions & 5 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
# Default recipe: list all available recipes when you run `just` with no args
default:
@just --list

# List all available recipes
list:
@just --list

# Ensure Rust code is formatted
fmt:
cargo fmt

# Check if Rust code is formatted (used in CI)
fmt-check:
cargo fmt -- --check
cargo fmt --all -- --check

# Run Clippy with warnings treated as errors
clippy:
cargo clippy -- -D warnings

# Scan Cargo.toml files for unused dependencies
machete:
cargo machete

# Run Prettier on Markdown files
prettier:
npx prettier --write "**/*.md"
Expand All @@ -18,17 +30,18 @@ prettier:
prettier-check:
npx prettier --check "**/*.md"

# CI task: check formatting and linting and all tests
ci: fmt-check clippy prettier-check test-python test-rust
# CI task: check formatting, linting, unused deps, and all tests
ci: fmt-check clippy machete prettier-check test-python test-rust

# Run python tests.
# Requires an active Python venv (run `source bindings/python/.venv/bin/activate`
# first). `maturin develop` builds the extension and installs it into the
# active venv so the tests exercise the current Rust code, not a stale wheel.
#
# Run python tests
test-python:
maturin develop --manifest-path bindings/python/Cargo.toml
sh bindings/python/tests/run.sh

# Run rust tests
test-rust:
cargo test --all-features
cargo test --all-features
2 changes: 1 addition & 1 deletion bindings/python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "codelists_rs"
version = "0.1.0"
edition = "2021"
edition = "2024"

[lib]
name = "codelists_rs"
Expand Down
4 changes: 2 additions & 2 deletions bindings/python/src/codelist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ use codelist_rs::{
use codelist_validator_rs::validator::Validator;
use indexmap::IndexSet;
use pyo3::{
PyErr, PyResult,
exceptions::PyValueError,
prelude::*,
types::{PyDict, PySet},
PyErr, PyResult,
};
use regex::Regex;

Expand Down Expand Up @@ -53,7 +53,7 @@ impl PyCodeList {
_ => {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Invalid codelist type: {codelist_type}"
)))
)));
}
};

Expand Down
2 changes: 1 addition & 1 deletion bindings/python/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl PyCodeListFactory {
_ => {
return Err(PyValueError::new_err(format!(
"Invalid codelist type: {codelist_type}"
)))
)));
}
};

Expand Down
4 changes: 2 additions & 2 deletions bindings/r/src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
name = 'codelist'
publish = false
version = '0.1.0'
edition = '2021'
rust-version = '1.65'
edition = '2024'
rust-version = '1.85'

[lib]
crate-type = [ 'staticlib' ]
Expand Down
4 changes: 1 addition & 3 deletions rust/codelist-builder-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
[package]
name = "codelist-builder-rs"
version = "0.1.0"
edition = "2021"
edition = "2024"
authors = ["Caroline Morton <caroline@parakeetconsulting.com>"]
description = "Builder library for medical codelists"

[dependencies]
codelist-rs = { path = "../codelist-rs" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = { version = "2.0.9" }
thiserror-ext = { version = "0.2.1" }
reqwest = { version = "0.12.2" }
csv = { version = "1.3.1" }
async-trait = "0.1"

[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }
Expand Down
2 changes: 1 addition & 1 deletion rust/codelist-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "codelist-rs"
version = "0.1.0"
edition = "2021"
edition = "2024"
authors = ["Caroline Morton <caroline@parakeetconsulting.com>", "Emma Bagshaw"]
description = "Base library for medical code list handling"

Expand Down
6 changes: 1 addition & 5 deletions rust/codelist-rs/src/codelist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,7 @@ impl CodeList {
/// found
pub fn remove_entry(&mut self, code: &str) -> Result<(), CodeListError> {
let removed = self.entries.remove(code);
if removed.is_some() {
Ok(())
} else {
Err(CodeListError::entry_not_found(code))
}
if removed.is_some() { Ok(()) } else { Err(CodeListError::entry_not_found(code)) }
}

/// Get the full entries of the codelist, including code, optional term and
Expand Down
81 changes: 46 additions & 35 deletions rust/codelist-rs/src/codelist_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,17 +290,16 @@ impl CodeListFactory {
let path = entry.path();

// Skips if not csv/json
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if ext == "csv" || ext == "json" {
if let Some(path_str) = path.to_str() {
// TODO: We are using the file name as the codelist name, but this may not
// be the best approach
if let Ok(codelist) =
self.load_codelist_from_file(folder_path.to_string(), path_str)
{
codelists.push(codelist);
}
}
if let Some(ext) = path.extension().and_then(|e| e.to_str())
&& (ext == "csv" || ext == "json")
&& let Some(path_str) = path.to_str()
{
// TODO: We are using the file name as the codelist name, but this may not
// be the best approach
if let Ok(codelist) =
self.load_codelist_from_file(folder_path.to_string(), path_str)
{
codelists.push(codelist);
}
}
}
Expand Down Expand Up @@ -482,18 +481,24 @@ C03,Test Disease 3,Description 3";
assert_eq!(codelist.entries.len(), 3);

// Test individual entries exist
assert!(codelist
.entries
.iter()
.any(|e| e.0 == "A01" && e.1 .0 == Some("Test Disease 1".to_string())));
assert!(codelist
.entries
.iter()
.any(|e| e.0 == "B02" && e.1 .0 == Some("Test Disease 2".to_string())));
assert!(codelist
.entries
.iter()
.any(|e| e.0 == "C03" && e.1 .0 == Some("Test Disease 3".to_string())));
assert!(
codelist
.entries
.iter()
.any(|e| e.0 == "A01" && e.1.0 == Some("Test Disease 1".to_string()))
);
assert!(
codelist
.entries
.iter()
.any(|e| e.0 == "B02" && e.1.0 == Some("Test Disease 2".to_string()))
);
assert!(
codelist
.entries
.iter()
.any(|e| e.0 == "C03" && e.1.0 == Some("Test Disease 3".to_string()))
);

assert!(!codelist.codelist_options.allow_duplicates);
assert_eq!(codelist.codelist_options.code_column_name, "code".to_string());
Expand Down Expand Up @@ -690,18 +695,24 @@ A01"; // Missing columns
assert_eq!(codelist.entries.len(), 3);

// Test individual entries exist
assert!(codelist
.entries
.iter()
.any(|e| e.0 == "A01" && e.1 .0 == Some("Test Disease 1".to_string())));
assert!(codelist
.entries
.iter()
.any(|e| e.0 == "B02" && e.1 .0 == Some("Test Disease 2".to_string())));
assert!(codelist
.entries
.iter()
.any(|e| e.0 == "C03" && e.1 .0 == Some("Test Disease 3".to_string())));
assert!(
codelist
.entries
.iter()
.any(|e| e.0 == "A01" && e.1.0 == Some("Test Disease 1".to_string()))
);
assert!(
codelist
.entries
.iter()
.any(|e| e.0 == "B02" && e.1.0 == Some("Test Disease 2".to_string()))
);
assert!(
codelist
.entries
.iter()
.any(|e| e.0 == "C03" && e.1.0 == Some("Test Disease 3".to_string()))
);

assert!(!codelist.codelist_options.allow_duplicates);
assert_eq!(codelist.codelist_options.code_column_name, "code".to_string());
Expand Down
10 changes: 8 additions & 2 deletions rust/codelist-rs/src/metadata/categorisation_and_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,10 @@ mod tests {
let mut categorisation_and_usage = test_categorisation_and_usage_all_some();
let error = categorisation_and_usage.add_license("license1".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "License already exists: Unable to add license license1. Please use update license instead.");
assert_eq!(
error_string,
"License already exists: Unable to add license license1. Please use update license instead."
);
Ok(())
}

Expand All @@ -277,7 +280,10 @@ mod tests {
let mut categorisation_and_usage = test_categorisation_and_usage_all_none();
let error = categorisation_and_usage.update_license("example".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "License does not exist: Unable to update license example. Please use add license instead.");
assert_eq!(
error_string,
"License does not exist: Unable to update license example. Please use add license instead."
);
Ok(())
}

Expand Down
20 changes: 16 additions & 4 deletions rust/codelist-rs/src/metadata/purpose_and_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,10 @@ mod tests {
let error =
purpose_and_context.add_target_audience("Target Audience".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Target audience already exists: Unable to add target audience. Please use update target audience instead.");
assert_eq!(
error_string,
"Target audience already exists: Unable to add target audience. Please use update target audience instead."
);
Ok(())
}

Expand All @@ -303,7 +306,10 @@ mod tests {
let error =
purpose_and_context.update_target_audience("Target Audience".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Target audience does not exist: Unable to update target audience. Please use add target audience instead.");
assert_eq!(
error_string,
"Target audience does not exist: Unable to update target audience. Please use add target audience instead."
);
Ok(())
}

Expand Down Expand Up @@ -340,7 +346,10 @@ mod tests {
let mut purpose_and_context = create_test_purpose_and_context_all_params_are_some();
let error = purpose_and_context.add_use_context("Use Context".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Use context already exists: Unable to add use context. Please use update use context instead.");
assert_eq!(
error_string,
"Use context already exists: Unable to add use context. Please use update use context instead."
);
Ok(())
}

Expand All @@ -357,7 +366,10 @@ mod tests {
let mut purpose_and_context = create_test_purpose_and_context_all_params_are_none();
let error = purpose_and_context.update_use_context("Use Context".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Use context does not exist: Unable to update use context. Please use add use context instead.");
assert_eq!(
error_string,
"Use context does not exist: Unable to update use context. Please use add use context instead."
);
Ok(())
}

Expand Down
20 changes: 16 additions & 4 deletions rust/codelist-rs/src/metadata/validation_and_review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,10 @@ mod tests {
let mut validation_and_review = test_validation_and_review_all_params_are_some_or_true();
let error = validation_and_review.add_review_date(chrono::Utc::now()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Review date already exists: Unable to add review date. Please use update review date instead.");
assert_eq!(
error_string,
"Review date already exists: Unable to add review date. Please use update review date instead."
);
Ok(())
}

Expand All @@ -414,7 +417,10 @@ mod tests {
let mut validation_and_review = test_validation_and_review_all_params_are_none();
let error = validation_and_review.update_review_date(chrono::Utc::now()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Review date does not exist: Unable to update review date. Please use add review date instead.");
assert_eq!(
error_string,
"Review date does not exist: Unable to update review date. Please use add review date instead."
);
Ok(())
}

Expand Down Expand Up @@ -513,7 +519,10 @@ mod tests {
let error =
validation_and_review.add_validation_notes("Validation Notes".to_string()).unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Validation notes already exist: Unable to add validation notes. Please use update validation notes instead.");
assert_eq!(
error_string,
"Validation notes already exist: Unable to add validation notes. Please use update validation notes instead."
);
Ok(())
}

Expand All @@ -536,7 +545,10 @@ mod tests {
.update_validation_notes("Validation Notes".to_string())
.unwrap_err();
let error_string = error.to_string();
assert_eq!(error_string, "Validation notes do not exist: Unable to update validation notes. Please use add validation notes instead.");
assert_eq!(
error_string,
"Validation notes do not exist: Unable to update validation notes. Please use add validation notes instead."
);
Ok(())
}

Expand Down
Loading
Loading