From 427bc15b50c5e6225592dd08f321d7fea3fc77cc Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Fri, 5 Dec 2025 13:23:59 +0100 Subject: [PATCH 1/4] Work on `Pointer`. --- src/tree/mod.rs | 2 +- src/tree/pointer.rs | 277 +++++++++++++++++++++++++++++++++++++++----- src/tree/utils.rs | 18 ++- 3 files changed, 255 insertions(+), 42 deletions(-) diff --git a/src/tree/mod.rs b/src/tree/mod.rs index 2912779..a26bcb6 100644 --- a/src/tree/mod.rs +++ b/src/tree/mod.rs @@ -3,4 +3,4 @@ pub mod node; pub mod node_repository; pub mod pointer; pub mod traits; -pub(crate) mod utils; +mod utils; diff --git a/src/tree/pointer.rs b/src/tree/pointer.rs index 2cf02ae..b28beb7 100644 --- a/src/tree/pointer.rs +++ b/src/tree/pointer.rs @@ -1,47 +1,164 @@ -use crate::tree::utils::{escape, unescape}; +use super::utils::{escape, unescape}; +use std::borrow::Cow; use std::fmt::Display; +/// Separator of reference tokens of the JSON pointer. +pub const TOKEN_SEP: char = '/'; + /// A struct representing a JSON Pointer (RFC 6901). /// -/// This internally stores the pointer as an escaped string (e.g., "/a/~1b"). +/// This internally stores the pointer as escaped string (e.g., "/a/~1b"). +/// +/// # Creation +/// +/// A `Pointer` can be created from an escaped string or from iterator of reference tokens (will be escaped): +/// +/// ``` +/// use phenolint::tree::pointer::Pointer; +/// +/// // from escaped string +/// let a = Pointer::from("/a/~1b"); +/// // from reference tokens +/// let b = Pointer::from_iter(["a", "/b"]); +/// +/// assert_eq!(a, b); +/// ``` +/// +/// Alternatively, a root pointer can be obtained from [`Pointer::at_root`]. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Pointer(String); -impl Pointer { - pub fn new(location: &str) -> Self { - let mut location = location.to_string(); - - location = escape(&location); +/// Compare the `Pointer` with an escaped `&str`. +/// +/// ``` +/// use phenolint::tree::pointer::Pointer; +/// +/// let ptr = Pointer::from("/path/to/0/resource"); +/// +/// assert_eq!(&ptr, "/path/to/0/resource") +/// ``` +impl PartialEq for Pointer { + fn eq(&self, other: &str) -> bool { + self.0.as_str() == other + } +} - if !location.is_empty() && !location.starts_with("/") && !location.starts_with("~1") { - location = format!("/{}", location); +/// Create a `Pointer` from an escaped `&str` JSON pointer. +/// +/// # Example +/// +/// Create a pointer: +/// +/// ``` +/// use phenolint::tree::pointer::Pointer; +/// +/// let ptr = Pointer::from("/path/to/0/resource"); +/// +/// assert_eq!(&ptr, "/path/to/0/resource"); +/// ``` +/// +/// The empty `&str` creates a root pointer (also available via [`Pointer::at_root`]): +/// +/// ``` +/// # use phenolint::tree::pointer::Pointer; +/// # +/// assert!(Pointer::from("").is_root()); +/// ``` +impl From<&str> for Pointer { + fn from(value: &str) -> Self { + let mut location = value.to_string(); + + if !location.is_empty() && !location.starts_with(TOKEN_SEP) && !location.starts_with("~1") { + location.insert(0, TOKEN_SEP); } Self(location) } +} + +/// Create a `Pointer` from an iterator of resource segments. +/// The segments are escaped according to the JSON Pointer specification. +/// +/// # Examples +/// +/// ``` +/// use phenolint::tree::pointer::Pointer; +/// +/// let ptr: Pointer = Pointer::from_iter(["a", "b", "c"]); +/// +/// assert_eq!(&ptr, "/a/b/c"); +/// ``` +/// +/// The resource tokens are properly escaped: +/// +/// ``` +/// # use phenolint::tree::pointer::Pointer; +/// # +/// let ptr: Pointer = ["a", "b/c", "~d"].into_iter().collect(); +/// +/// assert_eq!(&ptr, "/a/b~1c/~0d"); +/// ``` +impl<'a> FromIterator<&'a str> for Pointer { + fn from_iter>(iter: T) -> Self { + let mut buf = String::new(); + + iter.into_iter().map(escape).for_each(|val| { + buf.push(TOKEN_SEP); + buf.push_str(val.as_ref()); + }); + + Self(buf) + } +} + +impl Pointer { + #[deprecated(since = "0.1.0", note = "Use From<&str> instead")] + pub fn new(location: &str) -> Self { + Self::from(location) + } pub fn at_root() -> Self { Self(String::new()) } + #[deprecated( + since = "0.1.0", + note = "Specific to a Phenopacket Schema building block" + )] pub fn at_meta_data() -> Self { Self::new("metaData") } + #[deprecated( + since = "0.1.0", + note = "Specific to a Phenopacket Schema building block" + )] pub fn at_resources() -> Self { let mut mtd_ptr = Pointer::at_meta_data(); mtd_ptr.down("resources"); mtd_ptr } + #[deprecated( + since = "0.1.0", + note = "Specific to a Phenopacket Schema building block" + )] pub fn at_phenotypes() -> Self { Self::new("phenotypicFeatures") } + #[deprecated( + since = "0.1.0", + note = "Specific to a Phenopacket Schema building block" + )] pub fn at_subject() -> Self { Self::new("subject") } + #[deprecated( + since = "0.1.0", + note = "Specific to a Phenopacket Schema building block" + )] pub fn at_vital_status() -> Self { let mut ptr = Pointer::at_subject(); ptr.down("vitalStatus"); @@ -50,15 +167,33 @@ impl Pointer { /// Returns the final segment (tip) of the pointer path. /// - /// For example, if the pointer represents `"/user/name"`, + /// # Returns + /// A decoded string of the last path segment. + /// + /// # Example + /// + /// If the pointer represents `"/user/name"`, /// this returns `"name"`. + /// ``` + /// use phenolint::tree::pointer::Pointer; + /// + /// let mut ptr = Pointer::from("/user/name"); + /// + /// assert_eq!(ptr.get_tip(), "name"); + /// ``` + /// /// If the pointer is empty or at the root, it returns an empty string. /// - /// # Returns - /// A decoded string of the last path segment. - pub fn get_tip(&self) -> String { - let tip = self.0.split("/").last().unwrap_or(""); - tip.to_string() + /// ``` + /// # use phenolint::tree::pointer::Pointer; + /// # + /// let mut ptr = Pointer::at_root(); + /// + /// assert_eq!(ptr.get_tip(), ""); + /// ``` + /// + pub fn get_tip(&self) -> &str { + self.0.split(TOKEN_SEP).next_back().unwrap_or("") } /// Moves the pointer one level up the hierarchy. @@ -70,13 +205,30 @@ impl Pointer { /// A mutable reference to `self` (for chaining). /// /// # Example - /// ```ignore - /// let mut ptr = Pointer("/user/name".into()); + /// + /// ``` + /// use phenolint::tree::pointer::Pointer; + /// + /// let mut ptr = Pointer::from("/user/name"); + /// /// ptr.up(); + /// /// assert_eq!(ptr.position(), "/user"); /// ``` + /// + /// Going up from a root is a no-op: + /// + /// ``` + /// # use phenolint::tree::pointer::Pointer; + /// # + /// let mut ptr = Pointer::at_root(); + /// + /// ptr.up(); + /// + /// assert_eq!(ptr.position(), ""); + /// ``` pub fn up(&mut self) -> &mut Self { - if let Some(pos) = self.0.rfind('/') { + if let Some(pos) = self.0.rfind(TOKEN_SEP) { self.0.truncate(pos); } self @@ -94,15 +246,20 @@ impl Pointer { /// A mutable reference to `self` (for chaining). /// /// # Example - /// ```ignore - /// let mut ptr = Pointer(String::new()); - /// ptr.step("user").step("name"); - /// assert_eq!(ptr.position(), "/user/name"); + /// ``` + /// use phenolint::tree::pointer::Pointer; + /// + /// let mut ptr = Pointer::at_root(); + /// + /// ptr.down("path").down("to").down(0).down("resource"); + /// + /// assert_eq!(ptr.position(), "/path/to/0/resource"); /// ``` pub fn down(&mut self, step: S) -> &mut Self { let step = step.to_string(); let step = escape(&step); - self.0 = format!("{}/{}", self.0, step); + self.0.push(TOKEN_SEP); + self.0.push_str(&step); self } @@ -120,10 +277,22 @@ impl Pointer { /// Resets the pointer to the root position (`""`). /// + /// # Example + /// + /// ``` + /// use phenolint::tree::pointer::Pointer; + /// + /// let mut ptr = Pointer::from("/path/to/0/resource"); + /// + /// ptr.root(); + /// + /// assert!(ptr.is_root()); + /// ``` + /// /// # Returns /// A mutable reference to `self` (for chaining). pub fn root(&mut self) -> &mut Self { - self.0 = "".to_owned(); + self.0.clear(); self } @@ -135,8 +304,39 @@ impl Pointer { self.0.is_empty() } - pub fn segments(&self) -> impl Iterator + '_ { - self.0.split('/').skip(1).map(unescape) + #[deprecated(since = "0.1.0", note = "Use iter_segments() instead")] + pub fn segments(&self) -> impl Iterator> { + self.iter_segments() + } + + /// Evaluate the pointer into an iterator over reference tokens. + /// + /// ``` + /// use phenolint::tree::pointer::Pointer; + /// + /// let ptr = Pointer::from("path/to/0/resource"); + /// + /// let tokens: Vec<_> = ptr.segments().collect(); + /// + /// assert_eq!(&tokens, &["path", "to", "0", "resource"]); + /// ``` + /// + /// The tokens are properly unescaped: + /// + /// ``` + /// # use phenolint::tree::pointer::Pointer; + /// # + /// let src = ["path", "T/O", "~resource", "~1"]; + /// let ptr = Pointer::from_iter(src.iter().cloned()); + /// + /// assert_eq!(&ptr, "/path/T~1O/~0resource/~01"); + /// + /// let tokens: Vec<_> = ptr.segments().collect(); + /// + /// assert_eq!(&tokens, &src); + /// ``` + pub fn iter_segments(&self) -> impl Iterator> { + self.0.split(TOKEN_SEP).skip(1).map(unescape) } } @@ -171,6 +371,7 @@ mod tests { } #[rstest] + #[ignore] fn test_new_escapes_special_chars() { let ptr = Pointer::new("/foo/a~b/c/d"); // Should escape ~ to ~0 and / to ~1 @@ -309,28 +510,28 @@ mod tests { #[rstest] fn test_segments_empty() { let ptr = Pointer::new(""); - let segments: Vec = ptr.segments().collect(); + let segments: Vec<_> = ptr.segments().collect(); assert_eq!(segments, Vec::::new()); } #[rstest] fn test_segments_single() { let ptr = Pointer::new("/foo"); - let segments: Vec = ptr.segments().collect(); + let segments: Vec<_> = ptr.segments().collect(); assert_eq!(segments, vec!["foo"]); } #[rstest] fn test_segments_multiple() { let ptr = Pointer::new("/foo/bar/baz"); - let segments: Vec = ptr.segments().collect(); + let segments: Vec<_> = ptr.segments().collect(); assert_eq!(segments, vec!["foo", "bar", "baz"]); } #[rstest] fn test_segments_with_escaped_chars() { let ptr = Pointer::new("/foo/a~0b/c~1d"); - let segments: Vec = ptr.segments().collect(); + let segments: Vec<_> = ptr.segments().collect(); // Segments should be unescaped assert_eq!(segments, vec!["foo", "a~b", "c/d"]); } @@ -385,7 +586,7 @@ mod tests { fn test_empty_segment() { let ptr = Pointer::new("//"); // Should handle empty segments - let segments: Vec = ptr.segments().collect(); + let segments: Vec<_> = ptr.segments().collect(); assert_eq!(segments.len(), 2); } @@ -412,4 +613,18 @@ mod tests { assert!(ptr.position().contains("~0")); assert!(ptr.position().contains("~1")); } + + #[test] + fn from_iterator_base() { + let ptr: Pointer = Pointer::from_iter(["a", "b", "c"]); + + assert_eq!(&ptr, "/a/b/c"); + } + + #[test] + fn from_iterator_tokens_are_escaped() { + let ptr: Pointer = ["a", "b/c", "~d"].into_iter().collect(); + + assert_eq!(&ptr, "/a/b~1c/~0d"); + } } diff --git a/src/tree/utils.rs b/src/tree/utils.rs index d5f701a..dda9d5e 100644 --- a/src/tree/utils.rs +++ b/src/tree/utils.rs @@ -1,26 +1,24 @@ +use std::borrow::Cow; + /// Escapes a string segment for use in a JSON Pointer. /// /// Replaces "~" with "~0" and "/" with "~1". -pub(crate) fn escape(step: &str) -> String { - if is_escaped(step) { - step.to_string() - } else { - step.replace("~", "~0").replace("/", "~1") - } +pub(super) fn escape(step: &str) -> Cow<'_, str> { + step.replace("~", "~0").replace("/", "~1").into() } /// Unescapes a JSON Pointer segment. /// /// Replaces "~1" with "/" and "~0" with "~". -pub(crate) fn unescape(step: &str) -> String { +pub(super) fn unescape(step: &str) -> Cow<'_, str> { if is_escaped(step) { - step.replace("~1", "/").replace("~0", "~") + step.replace("~1", "/").replace("~0", "~").into() } else { - step.to_string() + step.into() } } -pub(crate) fn is_escaped(step: &str) -> bool { +fn is_escaped(step: &str) -> bool { let mut chars = step.chars().peekable(); while let Some(c) = chars.next() { if c == '~' { From c70587c1de743e14c6216cb9619c077d7b4b4d52 Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Mon, 8 Dec 2025 21:48:12 +0100 Subject: [PATCH 2/4] Show examples for escaping and unescaping. --- src/tree/pointer.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tree/pointer.rs b/src/tree/pointer.rs index b28beb7..7a8ae6d 100644 --- a/src/tree/pointer.rs +++ b/src/tree/pointer.rs @@ -627,4 +627,16 @@ mod tests { assert_eq!(&ptr, "/a/b~1c/~0d"); } + + #[test] + #[ignore] + fn escaped_vs_unescaped() { + let ptr_from_escaped = Pointer::from("a/b~1c/~0d"); + let ptr_from_unescaped = Pointer::from("a/b/c/~d"); + let ptr_from_iter = Pointer::from_iter(["a", "b/c", "~d"]); + + eprintln!("Escaped: {:?}", ptr_from_escaped); + eprintln!("Unescaped: {:?}", ptr_from_unescaped); + eprintln!("From iter: {:?}", ptr_from_iter); + } } From b7fefb1a6fbd106a85b9e7aac079cb4d89283407 Mon Sep 17 00:00:00 2001 From: Daniel Danis Date: Tue, 9 Dec 2025 10:35:54 +0100 Subject: [PATCH 3/4] Update the docs. --- src/tree/pointer.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/tree/pointer.rs b/src/tree/pointer.rs index 7a8ae6d..6c57d2b 100644 --- a/src/tree/pointer.rs +++ b/src/tree/pointer.rs @@ -2,8 +2,8 @@ use super::utils::{escape, unescape}; use std::borrow::Cow; use std::fmt::Display; -/// Separator of reference tokens of the JSON pointer. -pub const TOKEN_SEP: char = '/'; +/// Separator of the reference tokens in the JSON pointer. +const TOKEN_SEP: char = '/'; /// A struct representing a JSON Pointer (RFC 6901). /// @@ -11,7 +11,8 @@ pub const TOKEN_SEP: char = '/'; /// /// # Creation /// -/// A `Pointer` can be created from an escaped string or from iterator of reference tokens (will be escaped): +/// A `Pointer` can be created from an escaped string or from an iterator of reference tokens +/// (the tokens will be escaped): /// /// ``` /// use phenolint::tree::pointer::Pointer; @@ -28,7 +29,12 @@ pub const TOKEN_SEP: char = '/'; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Pointer(String); -/// Compare the `Pointer` with an escaped `&str`. +/// Compare the `Pointer` with a `&str` representing +/// escaped reference tokens joined with a path separator (`/`). +/// +/// # Example +/// +/// Compare a value where no escaping is necessary: /// /// ``` /// use phenolint::tree::pointer::Pointer; @@ -37,6 +43,16 @@ pub struct Pointer(String); /// /// assert_eq!(&ptr, "/path/to/0/resource") /// ``` +/// +/// Compare a value that needs to be escaped: +/// +/// ``` +/// # use phenolint::tree::pointer::Pointer; +/// # +/// let ptr = Pointer::from_iter(["path", "t/o", "~resource"]); +/// +/// assert_eq!(&ptr, "/path/t~1o/~0resource") +/// ``` impl PartialEq for Pointer { fn eq(&self, other: &str) -> bool { self.0.as_str() == other From 04041a39709ca4ac02ef22fad6b08ff1a7186070 Mon Sep 17 00:00:00 2001 From: SmartMonkey Date: Wed, 10 Dec 2025 10:28:48 +0100 Subject: [PATCH 4/4] Linting --- src/parsing/parseable_nodes.rs | 4 +- src/patches/patch_engine.rs | 88 ++++++++------- src/rules/resources.rs | 6 +- src/tree/pointer.rs | 148 ++++++++----------------- tests/test_custom_rule.rs | 2 +- tests/test_disease_consistency_rule.rs | 4 +- 6 files changed, 100 insertions(+), 152 deletions(-) diff --git a/src/parsing/parseable_nodes.rs b/src/parsing/parseable_nodes.rs index e212c0f..dd998e6 100644 --- a/src/parsing/parseable_nodes.rs +++ b/src/parsing/parseable_nodes.rs @@ -84,7 +84,7 @@ impl ParsableNode for Disease { if let Value::Object(map) = &node.inner && node .pointer() - .segments() + .iter_segments() .into_iter() .any(|seg| seg.to_lowercase() == "diseases") && map.contains_key("term") @@ -102,7 +102,7 @@ impl ParsableNode for Diagnosis { if let Value::Object(map) = &node.inner && node .pointer() - .segments() + .iter_segments() .into_iter() .any(|seg| seg.to_lowercase() == "interpretations") && map.contains_key("disease") diff --git a/src/patches/patch_engine.rs b/src/patches/patch_engine.rs index 73c40bc..c9c6d47 100644 --- a/src/patches/patch_engine.rs +++ b/src/patches/patch_engine.rs @@ -103,12 +103,14 @@ impl PatchEngine { patches.sort_by(|p1, p2| match (p1, p2) { (PatchInstruction::Add { .. }, PatchInstruction::Remove { .. }) => Ordering::Less, (PatchInstruction::Remove { .. }, PatchInstruction::Add { .. }) => Ordering::Greater, - (PatchInstruction::Add { at: at1, .. }, PatchInstruction::Add { at: at2, .. }) => { - at1.segments().count().cmp(&at2.segments().count()) - } - (PatchInstruction::Remove { at: at1 }, PatchInstruction::Remove { at: at2 }) => { - at1.segments().count().cmp(&at2.segments().count()) - } + (PatchInstruction::Add { at: at1, .. }, PatchInstruction::Add { at: at2, .. }) => at1 + .iter_segments() + .count() + .cmp(&at2.iter_segments().count()), + (PatchInstruction::Remove { at: at1 }, PatchInstruction::Remove { at: at2 }) => at1 + .iter_segments() + .count() + .cmp(&at2.iter_segments().count()), _ => Ordering::Equal, }); } @@ -168,7 +170,7 @@ mod tests { let phenostr = sample_phenopacket(); let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/metaData"), + at: Pointer::from("/metaData"), value: json!({"created": "2024-01-01"}), }, vec![], @@ -187,7 +189,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/subject/timeAtLastEncounter"), + at: Pointer::from("/subject/timeAtLastEncounter"), value: json!({"age": "P30Y"}), }, vec![], @@ -206,7 +208,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Remove { - at: Pointer::new("/subject/dateOfBirth"), + at: Pointer::from("/subject/dateOfBirth"), }, vec![], )); @@ -223,7 +225,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Remove { - at: Pointer::new("/diseases/0/onset"), + at: Pointer::from("/diseases/0/onset"), }, vec![], )); @@ -241,8 +243,8 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Move { - from: Pointer::new("/subject/dateOfBirth"), - to: Pointer::new("/subject/birthDate"), + from: Pointer::from("/subject/dateOfBirth"), + to: Pointer::from("/subject/birthDate"), }, vec![], )); @@ -260,8 +262,8 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Move { - from: Pointer::new("/diseases/0/onset"), - to: Pointer::new("/ageOfOnset"), + from: Pointer::from("/diseases/0/onset"), + to: Pointer::from("/ageOfOnset"), }, vec![], )); @@ -279,8 +281,8 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Duplicate { - from: Pointer::new("/subject/id"), - to: Pointer::new("/subject/patientId"), + from: Pointer::from("/subject/id"), + to: Pointer::from("/subject/patientId"), }, vec![], )); @@ -298,8 +300,8 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Duplicate { - from: Pointer::new("/diseases/0/term"), - to: Pointer::new("/diagnosisTerm"), + from: Pointer::from("/diseases/0/term"), + to: Pointer::from("/diagnosisTerm"), }, vec![], )); @@ -318,11 +320,11 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/subject/karyotypicSex"), + at: Pointer::from("/subject/karyotypicSex"), value: Value::String("XY".to_string()), }, vec![PatchInstruction::Add { - at: Pointer::new("/subject/taxonomy"), + at: Pointer::from("/subject/taxonomy"), value: json!({"id": "NCBITaxon:9606", "label": "Homo sapiens"}), }], )); @@ -340,16 +342,16 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/metaData"), + at: Pointer::from("/metaData"), value: json!({"created": "2024-01-01"}), }, vec![ PatchInstruction::Remove { - at: Pointer::new("/subject/dateOfBirth"), + at: Pointer::from("/subject/dateOfBirth"), }, PatchInstruction::Move { - from: Pointer::new("/subject/sex"), - to: Pointer::new("/subject/gender"), + from: Pointer::from("/subject/sex"), + to: Pointer::from("/subject/gender"), }, ], )); @@ -369,10 +371,10 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Remove { - at: Pointer::new("/subject/sex"), + at: Pointer::from("/subject/sex"), }, vec![PatchInstruction::Add { - at: Pointer::new("/subject/gender"), + at: Pointer::from("/subject/gender"), value: Value::String("MALE".to_string()), }], )); @@ -390,11 +392,11 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Move { - from: Pointer::new("/diseases/0"), - to: Pointer::new("/primaryDiagnosis"), + from: Pointer::from("/diseases/0"), + to: Pointer::from("/primaryDiagnosis"), }, vec![PatchInstruction::Add { - at: Pointer::new("/primaryDiagnosis/confirmed"), + at: Pointer::from("/primaryDiagnosis/confirmed"), value: Value::Bool(true), }], )); @@ -423,7 +425,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/schemaVersion"), + at: Pointer::from("/schemaVersion"), value: Value::Number(Number::from_f64(2.0f64).unwrap()), }, vec![], @@ -441,11 +443,11 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Duplicate { - from: Pointer::new("/subject"), - to: Pointer::new("/backup"), + from: Pointer::from("/subject"), + to: Pointer::from("/backup"), }, vec![PatchInstruction::Remove { - at: Pointer::new("/subject/dateOfBirth"), + at: Pointer::from("/subject/dateOfBirth"), }], )); @@ -464,7 +466,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/phenotypicFeatures/0/severity"), + at: Pointer::from("/phenotypicFeatures/0/severity"), value: json!({"label": "severe"}), }, vec![], @@ -485,7 +487,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/diseases/0/onset/iso8601"), + at: Pointer::from("/diseases/0/onset/iso8601"), value: json!({"iso8601duration": "P10Y"}), }, vec![], @@ -506,7 +508,7 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Add { - at: Pointer::new("/notes"), + at: Pointer::from("/notes"), value: Value::String( "Patient has \"complex\" symptoms; requires care.".to_string(), ), @@ -526,12 +528,12 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Move { - from: Pointer::new("/subject/sex"), - to: Pointer::new("/subject/biologicalSex"), + from: Pointer::from("/subject/sex"), + to: Pointer::from("/subject/biologicalSex"), }, vec![PatchInstruction::Move { - from: Pointer::new("/subject/id"), - to: Pointer::new("/patientIdentifier"), + from: Pointer::from("/subject/id"), + to: Pointer::from("/patientIdentifier"), }], )); @@ -550,10 +552,10 @@ mod tests { let patch = Patch::new(NonEmptyVec::with_rest( PatchInstruction::Remove { - at: Pointer::new("/subject/sex"), + at: Pointer::from("/subject/sex"), }, vec![PatchInstruction::Add { - at: Pointer::new("/subject/sex"), + at: Pointer::from("/subject/sex"), value: Value::String("FEMALE".to_string()), }], )); @@ -569,7 +571,7 @@ mod tests { let minimal = json!({"id": "test"}); let patch = Patch::new(NonEmptyVec::with_single_entry(PatchInstruction::Add { - at: Pointer::new("/subject"), + at: Pointer::from("/subject"), value: json!({"id": "patient.1"}), })); diff --git a/src/rules/resources.rs b/src/rules/resources.rs index 315498a..0ae21ea 100644 --- a/src/rules/resources.rs +++ b/src/rules/resources.rs @@ -80,7 +80,7 @@ mod test_curies_have_resources { label: "Seizure".into(), }, Default::default(), - Pointer::new("/phenotypicFeatures/0/type"), + Pointer::from("/phenotypicFeatures/0/type"), )]; let resources = []; let data = (List(&ocs), List(&resources)); @@ -109,12 +109,12 @@ impl ReportFromContext for CuriesHaveResourcesReport { impl CompileReport for CuriesHaveResourcesReport { fn compile_report(&self, full_node: &dyn Node, lint_violation: &LintViolation) -> ReportSpecs { - let resources_ptr = Pointer::new("/metaData/resources"); + let resources_ptr = Pointer::from("/metaData/resources"); let span = if let Some(resources_range) = full_node.span_at(&resources_ptr).cloned() { resources_range } else { // `metaData` lacks the `resources` field itself. - let metadata_ptr = Pointer::new("/metaData"); + let metadata_ptr = Pointer::from("/metaData"); full_node.span_at(&metadata_ptr) .cloned() .expect("We assume `metaData` is always in the `Node` because we validate the basic phenopacket invariants before running this rule") diff --git a/src/tree/pointer.rs b/src/tree/pointer.rs index 6c57d2b..45fc39d 100644 --- a/src/tree/pointer.rs +++ b/src/tree/pointer.rs @@ -128,59 +128,10 @@ impl<'a> FromIterator<&'a str> for Pointer { } impl Pointer { - #[deprecated(since = "0.1.0", note = "Use From<&str> instead")] - pub fn new(location: &str) -> Self { - Self::from(location) - } - pub fn at_root() -> Self { Self(String::new()) } - #[deprecated( - since = "0.1.0", - note = "Specific to a Phenopacket Schema building block" - )] - pub fn at_meta_data() -> Self { - Self::new("metaData") - } - - #[deprecated( - since = "0.1.0", - note = "Specific to a Phenopacket Schema building block" - )] - pub fn at_resources() -> Self { - let mut mtd_ptr = Pointer::at_meta_data(); - mtd_ptr.down("resources"); - mtd_ptr - } - - #[deprecated( - since = "0.1.0", - note = "Specific to a Phenopacket Schema building block" - )] - pub fn at_phenotypes() -> Self { - Self::new("phenotypicFeatures") - } - - #[deprecated( - since = "0.1.0", - note = "Specific to a Phenopacket Schema building block" - )] - pub fn at_subject() -> Self { - Self::new("subject") - } - - #[deprecated( - since = "0.1.0", - note = "Specific to a Phenopacket Schema building block" - )] - pub fn at_vital_status() -> Self { - let mut ptr = Pointer::at_subject(); - ptr.down("vitalStatus"); - ptr - } - /// Returns the final segment (tip) of the pointer path. /// /// # Returns @@ -320,11 +271,6 @@ impl Pointer { self.0.is_empty() } - #[deprecated(since = "0.1.0", note = "Use iter_segments() instead")] - pub fn segments(&self) -> impl Iterator> { - self.iter_segments() - } - /// Evaluate the pointer into an iterator over reference tokens. /// /// ``` @@ -332,9 +278,9 @@ impl Pointer { /// /// let ptr = Pointer::from("path/to/0/resource"); /// - /// let tokens: Vec<_> = ptr.segments().collect(); + /// let tokens: Vec<_> = ptr.iter_segments().collect(); /// - /// assert_eq!(&tokens, &["path", "to", "0", "resource"]); + /// assert_eq!(&tokens.as_slice(), &["path", "to", "0", "resource"]); /// ``` /// /// The tokens are properly unescaped: @@ -347,9 +293,9 @@ impl Pointer { /// /// assert_eq!(&ptr, "/path/T~1O/~0resource/~01"); /// - /// let tokens: Vec<_> = ptr.segments().collect(); + /// let tokens: Vec<_> = ptr.iter_segments().collect(); /// - /// assert_eq!(&tokens, &src); + /// assert_eq!(&tokens.as_slice(), &src); /// ``` pub fn iter_segments(&self) -> impl Iterator> { self.0.split(TOKEN_SEP).skip(1).map(unescape) @@ -369,73 +315,73 @@ mod tests { use rstest::rstest; #[rstest] fn test_new_empty() { - let ptr = Pointer::new(""); + let ptr = Pointer::from(""); assert_eq!(ptr.position(), ""); assert!(ptr.is_root()); } #[rstest] fn test_new_with_leading_slash() { - let ptr = Pointer::new("/foo/bar"); + let ptr = Pointer::from("/foo/bar"); assert_eq!(ptr.position(), "/foo/bar"); } #[rstest] fn test_new_without_leading_slash() { - let ptr = Pointer::new("foo/bar"); + let ptr = Pointer::from("foo/bar"); assert_eq!(ptr.position(), "/foo/bar"); } #[rstest] #[ignore] fn test_new_escapes_special_chars() { - let ptr = Pointer::new("/foo/a~b/c/d"); + let ptr = Pointer::from("/foo/a~b/c/d"); // Should escape ~ to ~0 and / to ~1 assert_eq!(ptr.position(), "~1foo~1a~0b~1c~1d"); } #[rstest] fn test_new_with_slash_in_segment() { - let ptr = Pointer::new("a/b"); + let ptr = Pointer::from("a/b"); // The slash should be escaped assert!(ptr.position().contains("~1") || ptr.position() == "/a/b"); } #[rstest] fn test_get_tip_simple() { - let ptr = Pointer::new("/user/name"); + let ptr = Pointer::from("/user/name"); assert_eq!(ptr.get_tip(), "name"); } #[rstest] fn test_get_tip_root() { - let ptr = Pointer::new(""); + let ptr = Pointer::from(""); assert_eq!(ptr.get_tip(), ""); } #[rstest] fn test_get_tip_single_segment() { - let ptr = Pointer::new("/foo"); + let ptr = Pointer::from("/foo"); assert_eq!(ptr.get_tip(), "foo"); } #[rstest] fn test_get_tip_with_escaped_chars() { - let ptr = Pointer::new("/user/na~0me"); + let ptr = Pointer::from("/user/na~0me"); let tip = ptr.get_tip(); assert_eq!(tip, "na~0me"); } #[rstest] fn test_up_from_nested() { - let mut ptr = Pointer::new("/user/name/first"); + let mut ptr = Pointer::from("/user/name/first"); ptr.up(); assert_eq!(ptr.position(), "/user/name"); } #[rstest] fn test_up_multiple_times() { - let mut ptr = Pointer::new("/a/b/c/d"); + let mut ptr = Pointer::from("/a/b/c/d"); ptr.up(); assert_eq!(ptr.position(), "/a/b/c"); ptr.up(); @@ -448,7 +394,7 @@ mod tests { #[rstest] fn test_up_at_root() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.up(); assert_eq!(ptr.position(), ""); assert!(ptr.is_root()); @@ -456,28 +402,28 @@ mod tests { #[rstest] fn test_up_chaining() { - let mut ptr = Pointer::new("/a/b/c"); + let mut ptr = Pointer::from("/a/b/c"); ptr.up().up(); assert_eq!(ptr.position(), "/a"); } #[rstest] fn test_down_simple() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.down("user"); assert_eq!(ptr.position(), "/user"); } #[rstest] fn test_down_multiple() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.down("user").down("name"); assert_eq!(ptr.position(), "/user/name"); } #[rstest] fn test_down_with_special_chars() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.down("a~b"); // ~ should be escaped to ~0 assert_eq!(ptr.position(), "/a~0b"); @@ -485,20 +431,20 @@ mod tests { #[rstest] fn test_down_with_integer() { - let mut ptr = Pointer::new("/array"); + let mut ptr = Pointer::from("/array"); ptr.down(0); assert_eq!(ptr.position(), "/array/0"); } #[rstest] fn test_position() { - let ptr = Pointer::new("/foo/bar"); + let ptr = Pointer::from("/foo/bar"); assert_eq!(ptr.position(), "/foo/bar"); } #[rstest] fn test_root() { - let mut ptr = Pointer::new("/user/name"); + let mut ptr = Pointer::from("/user/name"); ptr.root(); assert_eq!(ptr.position(), ""); assert!(ptr.is_root()); @@ -506,61 +452,61 @@ mod tests { #[rstest] fn test_root_chaining() { - let mut ptr = Pointer::new("/a/b/c"); + let mut ptr = Pointer::from("/a/b/c"); ptr.root().down("new"); assert_eq!(ptr.position(), "/new"); } #[rstest] fn test_is_root_true() { - let ptr = Pointer::new(""); + let ptr = Pointer::from(""); assert!(ptr.is_root()); } #[rstest] fn test_is_root_false() { - let ptr = Pointer::new("/foo"); + let ptr = Pointer::from("/foo"); assert!(!ptr.is_root()); } #[rstest] fn test_segments_empty() { - let ptr = Pointer::new(""); - let segments: Vec<_> = ptr.segments().collect(); + let ptr = Pointer::from(""); + let segments: Vec<_> = ptr.iter_segments().collect(); assert_eq!(segments, Vec::::new()); } #[rstest] fn test_segments_single() { - let ptr = Pointer::new("/foo"); - let segments: Vec<_> = ptr.segments().collect(); + let ptr = Pointer::from("/foo"); + let segments: Vec<_> = ptr.iter_segments().collect(); assert_eq!(segments, vec!["foo"]); } #[rstest] fn test_segments_multiple() { - let ptr = Pointer::new("/foo/bar/baz"); - let segments: Vec<_> = ptr.segments().collect(); + let ptr = Pointer::from("/foo/bar/baz"); + let segments: Vec<_> = ptr.iter_segments().collect(); assert_eq!(segments, vec!["foo", "bar", "baz"]); } #[rstest] fn test_segments_with_escaped_chars() { - let ptr = Pointer::new("/foo/a~0b/c~1d"); - let segments: Vec<_> = ptr.segments().collect(); + let ptr = Pointer::from("/foo/a~0b/c~1d"); + let segments: Vec<_> = ptr.iter_segments().collect(); // Segments should be unescaped assert_eq!(segments, vec!["foo", "a~b", "c/d"]); } #[rstest] fn test_display_trait() { - let ptr = Pointer::new("/user/name"); + let ptr = Pointer::from("/user/name"); assert_eq!(format!("{}", ptr), "/user/name"); } #[rstest] fn test_clone() { - let ptr1 = Pointer::new("/foo/bar"); + let ptr1 = Pointer::from("/foo/bar"); let ptr2 = ptr1.clone(); assert_eq!(ptr1, ptr2); assert_eq!(ptr1.position(), ptr2.position()); @@ -568,21 +514,21 @@ mod tests { #[rstest] fn test_equality() { - let ptr1 = Pointer::new("/foo/bar"); - let ptr2 = Pointer::new("/foo/bar"); + let ptr1 = Pointer::from("/foo/bar"); + let ptr2 = Pointer::from("/foo/bar"); assert_eq!(ptr1, ptr2); } #[rstest] fn test_inequality() { - let ptr1 = Pointer::new("/foo/bar"); - let ptr2 = Pointer::new("/foo/baz"); + let ptr1 = Pointer::from("/foo/bar"); + let ptr2 = Pointer::from("/foo/baz"); assert_ne!(ptr1, ptr2); } #[rstest] fn test_complex_navigation() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.down("users") .down("john") .down("address") @@ -600,30 +546,30 @@ mod tests { #[rstest] fn test_empty_segment() { - let ptr = Pointer::new("//"); + let ptr = Pointer::from("//"); // Should handle empty segments - let segments: Vec<_> = ptr.segments().collect(); + let segments: Vec<_> = ptr.iter_segments().collect(); assert_eq!(segments.len(), 2); } #[rstest] fn test_numeric_string_segment() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.down("123"); assert_eq!(ptr.position(), "/123"); assert_eq!(ptr.get_tip(), "123"); } #[rstest] - fn test_unicode_segments() { - let mut ptr = Pointer::new(""); + fn test_unicode_iter_segments() { + let mut ptr = Pointer::from(""); ptr.down("ユーザー").down("名前"); assert_eq!(ptr.get_tip(), "名前"); } #[rstest] fn test_special_json_pointer_chars() { - let mut ptr = Pointer::new(""); + let mut ptr = Pointer::from(""); ptr.down("~/test"); assert!(ptr.position().contains("~0")); diff --git a/tests/test_custom_rule.rs b/tests/test_custom_rule.rs index 297f386..75acef8 100644 --- a/tests/test_custom_rule.rs +++ b/tests/test_custom_rule.rs @@ -108,7 +108,7 @@ fn test_custom_rule(json_phenopacket: Phenopacket) { .one_violation() .patch(Patch::new(NonEmptyVec::with_single_entry( PatchInstruction::Remove { - at: Pointer::new("/id"), + at: Pointer::from("/id"), }, ))) .build(); diff --git a/tests/test_disease_consistency_rule.rs b/tests/test_disease_consistency_rule.rs index c3b3ab2..e393926 100644 --- a/tests/test_disease_consistency_rule.rs +++ b/tests/test_disease_consistency_rule.rs @@ -56,7 +56,7 @@ mod tests { serde_json::to_string_pretty(&patched).unwrap(), )), patches: vec![Patch::new(NonEmptyVec::with_single_entry(Add { - at: Pointer::new("/diseases"), + at: Pointer::from("/diseases"), value: Value::Array(vec![serde_json::to_value(disease).unwrap()]), }))], message_snippets: vec![interpretation_id, "disease"], @@ -131,7 +131,7 @@ mod tests { n_violations: 1, patched_phenopacket: None, patches: vec![Patch::new(NonEmptyVec::with_single_entry(Add { - at: Pointer::new("/diseases"), + at: Pointer::from("/diseases"), value: Value::Array(vec![ serde_json::to_value(Disease { term: Some(OntologyClass {