Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a457746
Next development iteration `0.7.2-dev0`.
ielis Dec 4, 2025
cc1fd5d
Add a doctest.
ielis Jan 12, 2026
d3195c4
Update docs.
ielis Jan 12, 2026
be596a3
Merge pull request #61 from P2GX/move-to-p2gx
ielis Jan 12, 2026
c3f297d
Merge branch 'development' into prefix-partial-eq-doctest
ielis Jan 12, 2026
8421826
Add serialization functions to serialize TermId to/from curie.
ielis Jan 13, 2026
06d3d53
Update readme with the `serde` feature.
ielis Jan 13, 2026
76f4a9a
One CI invocation tests all features.
ielis Jan 13, 2026
2b81b7c
Use `stable` Rust toolchain.
ielis Jan 13, 2026
41e3731
Merge pull request #62 from P2GX/serde-serialization
ielis Jan 13, 2026
9879626
Merge branch 'development' into prefix-partial-eq-doctest
ielis Jan 13, 2026
580f177
UO: Add test to validate ontology integration
SmartMonkey-git Jan 15, 2026
c4e2aa5
Merge branch 'master' into rr/add-uo-test
SmartMonkey-git Jan 15, 2026
885d339
Merge remote-tracking branch 'origin/development' into pr/SmartMonkey…
ielis Jan 16, 2026
645964c
Minor tweaks
ielis Jan 16, 2026
f507c35
Advertise support for the `UO` in README.
ielis Jan 16, 2026
8e22a1c
Merge pull request #64 from SmartMonkey-git/rr/add-uo-test
ielis Jan 16, 2026
ac61ea9
Merge branch 'development' into prefix-partial-eq-doctest
ielis Jan 16, 2026
2545bb6
Merge pull request #63 from P2GX/prefix-partial-eq-doctest
ielis Jan 16, 2026
04a7c38
Invoke `cargo fmt` in the CI.
ielis Jan 16, 2026
4fb1f02
Fix formatting errors.
ielis Jan 16, 2026
6e0c185
Merge pull request #65 from P2GX/enforce-formatting-in-ci
ielis Jan 16, 2026
94b3795
Make release `0.7.2`.
ielis Jan 16, 2026
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
70 changes: 48 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
name: Run tests

on:
push:
branches: [ development ]
pull_request:
branches: [ master, development ]
push:
branches: [development]
pull_request:
branches: [master, development]

jobs:
run-tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable]

steps:
- uses: actions/checkout@v4

- name: Install ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}

- name: Run tests
run: cargo test --release
run-tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable]

steps:
- uses: actions/checkout@v4

- name: Install ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}

- name: Run tests
run: cargo test --release

run-all-features:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "stable"

- name: Run tests
run: cargo test --features serde,pyo3 --release

format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: "stable"

- name: Check formatting of Rust code with rustfmt
uses: actions-rust-lang/rustfmt@v1.1.1
10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[package]
name = "ontolius"
version = "0.7.1"
version = "0.7.2"
description = "A fast and safe crate for working with biomedical ontologies."
keywords = ["ontology", "bioinformatics", "HPO", "MAxO", "GO"]
edition = "2021"

homepage = "https://github.com/ielis/ontolius"
repository = "https://github.com/ielis/ontolius"
homepage = "https://github.com/P2GX/ontolius"
repository = "https://github.com/P2GX/ontolius"
readme = "README.md"
license-file = "LICENSE"

Expand All @@ -23,16 +23,20 @@ pyo3 = { version = "0.24.1", optional = true, features = ["abi3-py310"] }
# The dependency restriction can be removed after the error is fixed.
# https://github.com/neo4j-labs/graph/issues/138
rayon = { version = "=1.10.0", optional = true }
serde = { version = "1.0.228", optional = true }

[dev-dependencies]
flate2 = "1.0.30"
criterion = "0.5.1"
serde = "1.0.228"
serde_test = "1.0.177"

[features]
default = ["obographs", "csr"]
csr = ["dep:graph_builder", "dep:rayon"]
obographs = ["dep:obographs-dev", "dep:curieosa"]
pyo3 = ["dep:pyo3"]
serde = ["dep:serde"]

[[bench]]
name = "hierarchy_io"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,10 @@ At this time, support for the following ontologies is tested:
* Human Phenotype Ontology (HPO)
* Gene Ontology (GO)
* Medical Action Ontology (MAxO)
* Units of Measurement Ontology (UO)

Other ontologies are very likely to work too.
In case of any problems, please let us know on our [Issue tracker](https://github.com/ielis/ontolius/issues).
In case of any problems, please let us know on our [Issue tracker](https://github.com/P2GX/ontolius/issues).


## Features
Expand All @@ -178,6 +179,7 @@ by default:
* `obographs` `(*)` - support loading Ontology from Obographs JSON file
* `pyo3` - include [`crate::py`] module with PyO3 bindings
to selected data structs to support using from Python
* `serde` - to provide (de)serialization functions to map [`crate::TermId`] to/from a curie (see `tests/test_serde.rs` for an example)


## Run tests
Expand Down
Binary file added resources/uo/uo.json.gz
Binary file not shown.
10 changes: 10 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ pub mod maxo {
pub static MEDICAL_ACTION: TermId = make_term_id(KnownPrefix::MAXO, 1, 7);
}

/// Constants for working with Unit of Measurement Ontology (UO).
pub mod uo {
use crate::{term_id::KnownPrefix, TermId};

use super::make_term_id;
/// [unit (UO:0000000)](http://purl.obolibrary.org/obo/UO_0000000)
/// is the root of all terms in the UO.
pub static UNIT: TermId = make_term_id(KnownPrefix::UO, 0, 7);
}

/// Constants for working with Gene Ontology (GO).
pub mod go {
use crate::{term_id::KnownPrefix, TermId};
Expand Down
64 changes: 64 additions & 0 deletions src/term_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,56 @@ impl Display for TermId {
}
}

/// Support writing out a [`TermId`] as a curie
/// when working with `serde`.
#[cfg(feature = "serde")]
mod serde {

use std::str::FromStr;

use super::TermId;

impl TermId {
pub fn serialize_as_curie<S>(term_id: &TermId, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Sadly, we must allocate a string to serialize a curie.
let curie = term_id.to_string();
serializer.serialize_str(&curie)
}

pub fn deserialize_from_curie<'de, D>(deserializer: D) -> Result<TermId, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TermIdCurieVisitor;

impl<'de> serde::de::Visitor<'de> for TermIdCurieVisitor {
type Value = TermId;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "{}", "a curie (e.g. \"HP:0001250\")")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match TermId::from_str(v) {
Ok(term_id) => Ok(term_id),
Err(_e) => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(v),
&self,
)),
}
}
}
deserializer.deserialize_str(TermIdCurieVisitor)
}
}
}

/// The representation of the prefix of a [`TermId`].
///
/// ### Examples
Expand Down Expand Up @@ -290,6 +340,15 @@ impl PartialEq for Prefix<'_> {
}
}

/// Prefix can be tested for equality with a `&str`.
///
/// ```
/// use ontolius::TermId;
///
/// let term_id: TermId = "HP:0001250".parse().unwrap();
///
/// assert!(&term_id.prefix() == "HP");
/// ```
impl PartialEq<str> for Prefix<'_> {
fn eq(&self, other: &str) -> bool {
match &self.0 .0 {
Expand Down Expand Up @@ -408,6 +467,7 @@ pub(crate) enum KnownPrefix {
CHEBI,
NCIT,
PMID,
UO,
}

impl PartialEq<str> for KnownPrefix {
Expand All @@ -424,6 +484,7 @@ impl PartialEq<str> for KnownPrefix {
KnownPrefix::CHEBI => other == "CHEBI",
KnownPrefix::NCIT => other == "NCIT",
KnownPrefix::PMID => other == "PMID",
KnownPrefix::UO => other == "UO",
}
}
}
Expand All @@ -442,6 +503,7 @@ impl Display for KnownPrefix {
KnownPrefix::CHEBI => f.write_str("CHEBI"),
KnownPrefix::NCIT => f.write_str("NCIT"),
KnownPrefix::PMID => f.write_str("PMID"),
KnownPrefix::UO => f.write_str("UO"),
}
}
}
Expand Down Expand Up @@ -474,6 +536,8 @@ impl TryFrom<&str> for KnownPrefix {
Ok(KnownPrefix::NCIT)
} else if value.starts_with("PMID") {
Ok(KnownPrefix::PMID)
} else if value.starts_with("UO") {
Ok(KnownPrefix::UO)
} else {
Err(())
}
Expand Down
7 changes: 6 additions & 1 deletion tests/test_common.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ontolius::common::{go, hpo, maxo};
use ontolius::common::{go, hpo, maxo, uo};

#[test]
fn hpo_commons_are_accessible() {
Expand All @@ -18,3 +18,8 @@ fn go_commons_are_accessible() {
fn maxo_commons_are_accessible() {
assert_eq!(maxo::MEDICAL_ACTION, ("MAXO", "0000001"))
}

#[test]
fn uo_commons_are_accessible() {
assert_eq!(uo::UNIT, ("UO", "0000000"))
}
85 changes: 85 additions & 0 deletions tests/test_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,88 @@ mod medical_action_ontology {
_ = maxo.version();
}
}

/// Unit of Measurement Ontology (UO) tests.
mod unit_measurement_ontology {

use std::fs::File;
use std::io::BufReader;
use std::sync::OnceLock;

use flate2::bufread::GzDecoder;
use ontolius::common::uo::UNIT;
use ontolius::io::OntologyLoaderBuilder;
use ontolius::ontology::csr::MinimalCsrOntology;
use ontolius::ontology::{HierarchyWalks, MetadataAware, OntologyTerms};
use ontolius::term::MinimalTerm;
use ontolius::TermId;

const UO_PATH: &str = "resources/uo/uo.json.gz";

fn uo() -> &'static MinimalCsrOntology {
static ONTOLOGY: OnceLock<MinimalCsrOntology> = OnceLock::new();
ONTOLOGY.get_or_init(|| {
let reader = GzDecoder::new(BufReader::new(
File::open(UO_PATH).expect("Obographs JSON file should exist"),
));
let loader = OntologyLoaderBuilder::new().obographs_parser().build();
loader
.load_from_read(reader)
.expect("Obographs JSON should be well formatted")
})
}

macro_rules! test_ancestors {
($($ontology: expr, $curie: expr, $expected: expr)*) => {
$(
let query: TermId = $curie.parse().unwrap();

let mut names: Vec<_> = $ontology
.iter_ancestor_ids(&query)
.map(|tid| $ontology.term_by_id(tid).map(MinimalTerm::name).unwrap())
.collect();
names.sort();
assert_eq!(
names,
$expected,
);
)*
};
}

#[test]
fn iter_ancestor_ids() {
let uo = uo();

test_ancestors!(
uo,
"UO:0010002", // millisiemens
&[
"conduction unit",
"electrical conduction unit",
"siemens based unit",
"unit"
]
);
test_ancestors!(
uo,
"UO:0000010", // second
&["base unit", "second based unit", "time unit", "unit"]
);
}

#[test]
fn we_get_expected_descendant_counts_for_uo_root() {
let uo = uo();

let descendant_count = uo.iter_descendant_ids(&UNIT).count();
assert_eq!(descendant_count, 549);
}

#[test]
fn version_parsing() {
let uo = uo();

assert_eq!(uo.version(), "2026-01-09");
}
}
Loading