use alloc::boxed::Box;
use alloc::vec::Vec;
use zerodds_opcua_gateway::data_value::{DataValue, Variant};
use zerodds_opcua_gateway::node_id::NodeId;
use zerodds_opcua_gateway::types::{LocalizedText, QualifiedName};
pub mod reference_types {
use zerodds_opcua_gateway::node_id::NodeId;
pub const REFERENCES: NodeId = NodeId::numeric(0, 31);
pub const HIERARCHICAL_REFERENCES: NodeId = NodeId::numeric(0, 33);
pub const HAS_CHILD: NodeId = NodeId::numeric(0, 34);
pub const ORGANIZES: NodeId = NodeId::numeric(0, 35);
pub const HAS_SUBTYPE: NodeId = NodeId::numeric(0, 45);
pub const HAS_PROPERTY: NodeId = NodeId::numeric(0, 46);
pub const HAS_COMPONENT: NodeId = NodeId::numeric(0, 47);
pub const HAS_TYPE_DEFINITION: NodeId = NodeId::numeric(0, 40);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeClass {
Unspecified,
Object,
Variable,
Method,
ObjectType,
VariableType,
ReferenceType,
DataType,
View,
}
impl NodeClass {
#[must_use]
pub const fn bit(self) -> u32 {
match self {
Self::Unspecified => 0,
Self::Object => 1,
Self::Variable => 2,
Self::Method => 4,
Self::ObjectType => 8,
Self::VariableType => 16,
Self::ReferenceType => 32,
Self::DataType => 64,
Self::View => 128,
}
}
#[must_use]
pub const fn as_i32(self) -> i32 {
self.bit() as i32
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NodeMeta {
pub node_id: NodeId,
pub node_class: NodeClass,
pub browse_name: QualifiedName,
pub display_name: LocalizedText,
pub type_definition: NodeId,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ReferenceRecord {
pub source: NodeId,
pub reference_type: NodeId,
pub is_forward: bool,
pub target: NodeId,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BrowseMatch {
pub reference_type: NodeId,
pub is_forward: bool,
pub target: NodeMeta,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MethodOutcome {
pub status_code: u32,
pub output_arguments: Vec<Variant>,
}
impl MethodOutcome {
#[must_use]
pub fn good(output_arguments: Vec<Variant>) -> Self {
Self {
status_code: 0,
output_arguments,
}
}
#[must_use]
pub fn fault(status_code: u32) -> Self {
Self {
status_code,
output_arguments: Vec::new(),
}
}
}
type MethodHandler = Box<dyn Fn(&[Variant]) -> MethodOutcome + Send + Sync>;
#[derive(Default)]
pub struct AddressSpace {
values: Vec<(NodeId, DataValue)>,
methods: Vec<(NodeId, MethodHandler)>,
nodes: Vec<NodeMeta>,
references: Vec<ReferenceRecord>,
}
impl core::fmt::Debug for AddressSpace {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AddressSpace")
.field("values", &self.values.len())
.field("methods", &self.methods.len())
.field("nodes", &self.nodes.len())
.field("references", &self.references.len())
.finish()
}
}
fn is_reference_subtype_of(reference: &NodeId, base: &NodeId) -> bool {
use reference_types::{
HAS_CHILD, HAS_COMPONENT, HAS_PROPERTY, HAS_SUBTYPE, HIERARCHICAL_REFERENCES, ORGANIZES,
REFERENCES,
};
if reference == base || *base == REFERENCES {
return true;
}
if *base == HIERARCHICAL_REFERENCES {
return [
ORGANIZES,
HAS_CHILD,
HAS_COMPONENT,
HAS_PROPERTY,
HAS_SUBTYPE,
]
.iter()
.any(|n| n == reference)
|| *reference == HIERARCHICAL_REFERENCES;
}
if *base == HAS_CHILD {
return [HAS_COMPONENT, HAS_PROPERTY, HAS_SUBTYPE]
.iter()
.any(|n| n == reference);
}
false
}
impl AddressSpace {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_value(&mut self, node_id: NodeId, value: DataValue) -> &mut Self {
if let Some(slot) = self.values.iter_mut().find(|(n, _)| *n == node_id) {
slot.1 = value;
} else {
self.values.push((node_id, value));
}
self
}
#[must_use]
pub fn value(&self, node_id: &NodeId) -> Option<&DataValue> {
self.values
.iter()
.find(|(n, _)| n == node_id)
.map(|(_, v)| v)
}
pub fn register_method<F>(&mut self, method_id: NodeId, handler: F) -> &mut Self
where
F: Fn(&[Variant]) -> MethodOutcome + Send + Sync + 'static,
{
let boxed: MethodHandler = Box::new(handler);
if let Some(slot) = self.methods.iter_mut().find(|(n, _)| *n == method_id) {
slot.1 = boxed;
} else {
self.methods.push((method_id, boxed));
}
self
}
#[must_use]
pub fn call(&self, method_id: &NodeId, input_arguments: &[Variant]) -> Option<MethodOutcome> {
self.methods
.iter()
.find(|(n, _)| n == method_id)
.map(|(_, h)| h(input_arguments))
}
pub fn add_node(&mut self, meta: NodeMeta) -> &mut Self {
if let Some(slot) = self.nodes.iter_mut().find(|n| n.node_id == meta.node_id) {
*slot = meta;
} else {
self.nodes.push(meta);
}
self
}
pub fn add_reference(
&mut self,
source: NodeId,
reference_type: NodeId,
target: NodeId,
) -> &mut Self {
self.references.push(ReferenceRecord {
source,
reference_type,
is_forward: true,
target,
});
self
}
#[must_use]
pub fn node_meta(&self, node_id: &NodeId) -> Option<&NodeMeta> {
self.nodes.iter().find(|n| n.node_id == *node_id)
}
#[must_use]
pub fn browse(
&self,
node_id: &NodeId,
direction: i32,
reference_type: Option<&NodeId>,
include_subtypes: bool,
node_class_mask: u32,
) -> Vec<BrowseMatch> {
let want_forward = direction == 0 || direction == 2;
let want_inverse = direction == 1 || direction == 2;
let mut out = Vec::new();
for r in &self.references {
let (matches_node, is_forward) = if r.source == *node_id {
(want_forward, true)
} else if r.target == *node_id {
(want_inverse, false)
} else {
(false, true)
};
if !matches_node {
continue;
}
if let Some(ft) = reference_type {
let ok = if include_subtypes {
is_reference_subtype_of(&r.reference_type, ft)
} else {
r.reference_type == *ft
};
if !ok {
continue;
}
}
let other = if is_forward { &r.target } else { &r.source };
let target_meta = self.node_meta(other).cloned().unwrap_or_else(|| NodeMeta {
node_id: other.clone(),
node_class: NodeClass::Unspecified,
browse_name: QualifiedName {
namespace_index: 0,
name: alloc::string::String::new(),
},
display_name: LocalizedText {
locale: None,
text: None,
},
type_definition: NodeId::numeric(0, 0),
});
if node_class_mask != 0 && (target_meta.node_class.bit() & node_class_mask) == 0 {
continue;
}
out.push(BrowseMatch {
reference_type: r.reference_type.clone(),
is_forward,
target: target_meta,
});
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use zerodds_opcua_gateway::data_value::VariantValue;
#[test]
fn values_set_get_replace() {
let mut a = AddressSpace::new();
let n = NodeId::numeric(1, 1);
a.set_value(
n.clone(),
DataValue::new_value(Variant::scalar(VariantValue::Int32(1)), 0, 0),
);
assert_eq!(
a.value(&n).unwrap().value,
Some(Variant::scalar(VariantValue::Int32(1)))
);
a.set_value(
n.clone(),
DataValue::new_value(Variant::scalar(VariantValue::Int32(2)), 0, 0),
);
assert_eq!(
a.value(&n).unwrap().value,
Some(Variant::scalar(VariantValue::Int32(2)))
);
assert!(a.value(&NodeId::numeric(1, 99)).is_none());
}
#[test]
fn method_call_dispatch() {
let mut a = AddressSpace::new();
let m = NodeId::numeric(1, 100);
a.register_method(m.clone(), |args| {
if let Some(Variant { value, .. }) = args.first() {
if let Some(VariantValue::Int32(x)) = value.first() {
return MethodOutcome::good(alloc::vec![Variant::scalar(VariantValue::Int32(
x * 2
))]);
}
}
MethodOutcome::fault(0x8000_0000)
});
let out = a
.call(&m, &[Variant::scalar(VariantValue::Int32(21))])
.expect("method");
assert_eq!(out.status_code, 0);
assert_eq!(
out.output_arguments[0],
Variant::scalar(VariantValue::Int32(42))
);
assert!(a.call(&NodeId::numeric(1, 7), &[]).is_none());
}
fn qn(name: &str) -> QualifiedName {
QualifiedName {
namespace_index: 1,
name: alloc::string::String::from(name),
}
}
fn graph() -> AddressSpace {
use reference_types::{HAS_COMPONENT, HAS_TYPE_DEFINITION, ORGANIZES};
let mut a = AddressSpace::new();
let objects = NodeId::numeric(0, 85);
let boiler = NodeId::numeric(1, 1);
let temp = NodeId::numeric(1, 2);
for (id, class, name) in [
(objects.clone(), NodeClass::Object, "Objects"),
(boiler.clone(), NodeClass::Object, "Boiler"),
(temp.clone(), NodeClass::Variable, "Temperature"),
] {
a.add_node(NodeMeta {
node_id: id,
node_class: class,
browse_name: qn(name),
display_name: LocalizedText {
locale: None,
text: Some(alloc::string::String::from(name)),
},
type_definition: NodeId::numeric(0, 58),
});
}
a.add_reference(objects, ORGANIZES, boiler.clone());
a.add_reference(boiler.clone(), HAS_COMPONENT, temp);
a.add_reference(boiler, HAS_TYPE_DEFINITION, NodeId::numeric(0, 58));
a
}
#[test]
fn browse_forward_and_inverse() {
let a = graph();
let boiler = NodeId::numeric(1, 1);
let fwd = a.browse(&boiler, 0, None, false, 0);
assert_eq!(fwd.len(), 2);
let inv = a.browse(&boiler, 1, None, false, 0);
assert_eq!(inv.len(), 1);
assert_eq!(inv[0].target.node_id, NodeId::numeric(0, 85));
assert!(!inv[0].is_forward);
}
#[test]
fn browse_subtype_filter_excludes_non_hierarchical() {
let a = graph();
let boiler = NodeId::numeric(1, 1);
let hits = a.browse(
&boiler,
0,
Some(&reference_types::HIERARCHICAL_REFERENCES),
true,
0,
);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].reference_type, reference_types::HAS_COMPONENT);
assert_eq!(hits[0].target.node_id, NodeId::numeric(1, 2));
}
#[test]
fn browse_node_class_mask_filters_targets() {
let a = graph();
let boiler = NodeId::numeric(1, 1);
let vars = a.browse(&boiler, 0, None, false, NodeClass::Variable.bit());
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].target.node_class, NodeClass::Variable);
}
}