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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Implemented accessor/utility methods for `ua::RelativePath`, `ua::BrowsePath`, and
`ua::RelativePathElement`.

## [0.12.1] - 2026-10-22

### Added
Expand Down
10 changes: 10 additions & 0 deletions src/ua/data_types/browse_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,14 @@ impl BrowsePath {
relative_path.clone_into_raw(&mut self.0.relativePath);
self
}

#[must_use]
pub fn starting_node(&self) -> &ua::NodeId {
ua::NodeId::raw_ref(&self.0.startingNode)
}

#[must_use]
pub fn relative_path(&self) -> &ua::RelativePath {
ua::RelativePath::raw_ref(&self.0.relativePath)
}
}
170 changes: 169 additions & 1 deletion src/ua/data_types/relative_path.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::ua;
use std::{fmt, ptr, slice::SliceIndex, str::FromStr};

use open62541_sys::{UA_RelativePath_parse, UA_RelativePath_print};

use crate::{DataType as _, Error, ua};

crate::data_type!(RelativePath);

Expand All @@ -9,4 +13,168 @@ impl RelativePath {
array.move_into_raw(&mut self.0.elementsSize, &mut self.0.elements);
self
}

/// Returns a view of the elements that make up this relative path.
///
/// Returns [`Some`] if the relative path element array is valid, otherwise [`None`].
#[must_use]
pub fn elements(&self) -> Option<&[ua::RelativePathElement]> {
// SAFETY: Lifetime of the slice is implicitly bound to the lifetime of the reference to self.
// Pointer validity is checked in `Array::slice_from_raw_parts()`.
unsafe { ua::Array::slice_from_raw_parts(self.0.elementsSize, self.0.elements) }
}

/// Returns a mutable view of the elements that make up this relative path.
///
/// Returns [`Some`] if the relative path element array is valid, otherwise [`None`].
#[must_use]
pub fn elements_mut(&mut self) -> Option<&mut [ua::RelativePathElement]> {
// SAFETY: Lifetime of the slice is implicitly bound to the lifetime of the reference to self.
// Pointer validity is checked in `Array::slice_from_raw_parts_mut()`.
unsafe { ua::Array::slice_from_raw_parts_mut(self.0.elementsSize, self.0.elements) }
}

/// Attempts to access the element at `index`.
///
/// Returns [`Some`] if the array is valid and `index` < [`RelativePath::len()`], otherwise [`None`]
#[must_use]
pub fn get<I>(&self, index: I) -> Option<&I::Output>
where
I: SliceIndex<[ua::RelativePathElement]>,
{
self.elements().and_then(|elements| elements.get(index))
}

/// Attempts to access the element at `index` mutably.
///
/// Returns [`Some`] if the array is valid and `index` < [`RelativePath::len()`], otherwise [`None`]
#[must_use]
pub fn get_mut<I>(&mut self, index: I) -> Option<&mut I::Output>
where
I: SliceIndex<[ua::RelativePathElement]>,
{
self.elements_mut()
.and_then(|elements| elements.get_mut(index))
}

/// Returns the number of elements in the relative path.
#[must_use]
pub const fn len(&self) -> usize {
self.0.elementsSize
}

/// Returns true if the relative path has no elements.
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.elementsSize == 0
}

/// Parses a relative path from a string, using the limited grammar implemented by open62541.
///
/// # Errors
/// Will return an error if the string is not parseable as a relative path.
///
/// See [UA_RelativePath_parse](https://open62541.org/doc/master/util.html#example-relativepaths) docs.
pub fn parse(path: &str) -> Result<Self, Error> {
let path = ua::String::new(path)?;
let mut parsed_path = Self::init();

let result =
unsafe { UA_RelativePath_parse(parsed_path.as_mut_ptr(), ptr::read(path.as_ptr())) };

let status_code = ua::StatusCode::new(result);

if status_code.is_good() {
Ok(parsed_path)
} else {
Err(Error::new(status_code))
}
}

pub fn iter(&self) -> impl ExactSizeIterator<Item = &ua::RelativePathElement> {
self.elements().unwrap_or(&[]).iter()
}

pub fn iter_mut(&mut self) -> impl ExactSizeIterator<Item = &mut ua::RelativePathElement> {
self.elements_mut().unwrap_or(&mut []).iter_mut()
}
}

impl FromStr for RelativePath {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}

impl fmt::Display for RelativePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut str = ua::String::null();

let status_code = ua::StatusCode::new(unsafe {
// SAFETY: `UA_RelativePath_print` will initialize the string if null.
// See UA_RelativePath_print docs (https://open62541.org/doc/master/util.html#example-relativepaths)
UA_RelativePath_print(&raw const self.0, str.as_mut_ptr())
});

// A lot of chained `UA_String_append` & `UA_String_escapeAppend`
// calls + an optimistic stack allocation w/ retry if the buffer is too small.
// SAFETY: `UA_RelativePath_print` will only return an error on a failed allocation.
debug_assert!(
status_code.is_good(),
"failed to print relative path: {status_code}"
);

str.fmt(f)
}
}

#[cfg(test)]
mod tests {
use super::RelativePath;
use crate::{DataType as _, ua};

#[test]
fn well_formed_path_parses_with_expected_output() {
let parsed = RelativePath::parse("/Objects<#!HasChild>Server")
.expect("well-formed `RelativePath` string should parse");

assert_eq!(parsed.len(), 2);

let elem = parsed.get(0).unwrap();
let expected = ua::RelativePathElement::init()
.with_include_subtypes(true)
.with_is_inverse(false)
.with_target_name(&ua::QualifiedName::new(0, "Objects"))
.with_reference_type_id(&ua::NodeId::numeric(0, 33));

assert_eq!(elem, &expected);

let elem = parsed.get(1).unwrap();
let expected = ua::RelativePathElement::init()
.with_include_subtypes(false)
.with_is_inverse(true)
.with_target_name(&ua::QualifiedName::new(0, "Server"))
.with_reference_type_id(&ua::NodeId::numeric(0, 34));

assert_eq!(elem, &expected);
}

#[test]
fn well_formed_path_survives_round_trip() {
const GOOD_PATH: &str = "<!Aggregates>2:PLC1/2:GVL_MAIN";
let parsed =
RelativePath::parse(GOOD_PATH).expect("well-formed `RelativePath` string should parse");

assert_eq!(parsed.len(), 2);
assert_eq!(parsed.to_string(), GOOD_PATH);
}

#[test]
fn malformed_path_fails_to_parse() {
const BAD_PATH: &str = "/!Bad!>/Path:1";
let result = RelativePath::parse(BAD_PATH);
assert!(result.is_err(), "parsing a bad path should fail");
}
}
20 changes: 20 additions & 0 deletions src/ua/data_types/relative_path_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,24 @@ impl RelativePathElement {
target_name.clone_into_raw(&mut self.0.targetName);
self
}

#[must_use]
pub fn target_name(&self) -> &ua::QualifiedName {
ua::QualifiedName::raw_ref(&self.0.targetName)
}

#[must_use]
pub const fn is_inverse(&self) -> bool {
self.0.isInverse
}

#[must_use]
pub const fn include_subtypes(&self) -> bool {
self.0.includeSubtypes
}

#[must_use]
pub fn reference_type_id(&self) -> &ua::NodeId {
ua::NodeId::raw_ref(&self.0.referenceTypeId)
}
}
Loading