Skip to content
Open
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
4 changes: 2 additions & 2 deletions rclrs-macros/src/parameter_set/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ fn handle_field_doc(field: &Field) -> String {
/// The two are kept apart because they do not carry the same authority. See [`default_value`].
#[derive(Clone, Copy)]
pub(crate) enum DefaultSource {
/// The value passed in for this one instance of the set, from a parent field's `default` or
/// from the caller of `declare_parameters`.
/// The value passed in for this one instance of the set, from a parent field's `default`,
/// from the caller of `declare_parameters`, or from one entry of a map.
Supplied,
/// The set's own `#[parameters(default = ...)]`.
Own,
Expand Down
19 changes: 17 additions & 2 deletions rclrs-macros/src/parameter_set/expand_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,28 @@ fn test_rejects_sequences_of_things_ros_has_no_array_type_for() {
}

#[test]
fn test_rejects_maps() {
fn test_rejects_a_map_of_plain_values() {
rejected_with(
"struct C { extra: HashMap<String, String> }",
&["no map parameter type"],
&["no map parameter type", "#[derive(ParameterSet)]"],
);
}

#[test]
fn test_rejects_a_map_whose_keys_are_not_names() {
rejected_with(
"struct C { sensors: HashMap<i64, SensorConfig> }",
&["have to be `String`"],
);
}

/// A map of parameter sets is how entries named by whoever configures the node are declared.
#[test]
fn test_accepts_a_map_of_parameter_sets() {
accepted("struct C { sensors: HashMap<String, SensorConfig> }");
accepted("struct C { sensors: BTreeMap<String, SensorConfig> }");
}

#[test]
fn test_rejects_nested_option() {
rejected_with(
Expand Down
30 changes: 24 additions & 6 deletions rclrs-macros/src/parameter_set/known_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,30 @@ pub(crate) fn shape_of(ty: &Type) -> TypeShape {
}
}

// A map is a recognised mistake rather than an unrecognised type, since no `DeclareField`
// implementation could ever make one work.
if key.starts_with("HashMap<") || key.starts_with("BTreeMap<") {
return TypeShape::Rejected(format!(
"`{key}` cannot be a ROS 2 parameter: ROS 2 has no map parameter type"
));
// A map is a parameter set per entry, with the entry names coming from the parameters the
// node was configured with. The values therefore have to be sets, and the keys have to be
// the names of those entries.
if let Some(arguments) = key
.strip_prefix("HashMap<")
.or_else(|| key.strip_prefix("BTreeMap<"))
.and_then(|k| k.strip_suffix('>'))
{
let (map_key, map_value) = arguments.split_once(',').unwrap_or((arguments, ""));

if map_key != "String" {
return TypeShape::Rejected(format!(
"the keys of a parameter map are the names its entries are declared under, so \
they have to be `String`, not `{map_key}`"
));
}

if NUMERIC_LEAVES.contains(&map_value) || OTHER_LEAVES.contains(&map_value) {
return TypeShape::Rejected(format!(
"`{key}` cannot be a ROS 2 parameter: ROS 2 has no map parameter type. A map \
field declares a parameter set for each of its entries, so its values have to \
be types with `#[derive(ParameterSet)]`"
));
}
}

// `Option<T>` is an optional parameter when `T` is a parameter type. `Option` of anything
Expand Down
1 change: 0 additions & 1 deletion rclrs/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1464,7 +1464,6 @@ impl NodeState {
}

/// Access to this node's parameter interface, for the parameter implementation itself.
#[cfg(test)]
pub(crate) fn parameter_interface(&self) -> &ParameterInterface {
&self.parameter
}
Expand Down
32 changes: 31 additions & 1 deletion rclrs/src/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
call_string_getter_with_rcl_node, rcl_bindings::*, Node, RclrsError, ENTITY_LIFECYCLE_MUTEX,
};
use std::{
collections::{btree_map::Entry, BTreeMap},
collections::{btree_map::Entry, BTreeMap, BTreeSet},
fmt::Debug,
sync::{Arc, Mutex, RwLock, Weak},
};
Expand Down Expand Up @@ -1383,6 +1383,32 @@ impl ParameterInterface {
.insert(name, ParameterStorage::Declared(storage));
}

/// The distinct names that appear directly under `prefix` in the parameter overrides.
///
/// For overrides `sensors.front.rate` and `sensors.rear.rate`, the names under `sensors` are
/// `front` and `rear`. This is how a map field, whose entries are named by whoever configures
/// the node, finds out what those names are. ROS 2 has no map parameter type, and overrides
/// are not otherwise visible until the parameter they name is declared.
pub(crate) fn override_names_under(&self, prefix: &str) -> BTreeSet<String> {
let scope = if prefix.is_empty() {
String::new()
} else {
format!("{prefix}.")
};

self.override_map
.range(scope.clone()..)
.take_while(|(name, _)| name.starts_with(&scope))
.filter_map(|(name, _)| {
let rest = &name[scope.len()..];
// Only a name with something after it is a namespace, and so an entry of the map.
// `sensors: 3` alongside `sensors.front.rate` is a different parameter entirely.
let (entry, _) = rest.split_once('.')?;
(!entry.is_empty()).then(|| entry.to_string())
})
.collect()
}

pub(crate) fn allow_undeclared(&self) {
self.parameter_map.lock().unwrap().allow_undeclared = true;
}
Expand All @@ -1392,6 +1418,10 @@ impl ParameterInterface {
#[path = "parameter/enum_set_tests.rs"]
mod enum_set_tests;

#[cfg(test)]
#[path = "parameter/map_tests.rs"]
mod map_tests;

#[cfg(test)]
#[path = "parameter/variant_tests.rs"]
mod variant_tests;
Expand Down
Loading
Loading