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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ impl Errors {
}
}

/// Whether anything has been recorded yet.
pub fn is_empty(&self) -> bool {
self.0.is_none()
}

/// Returns the accumulated errors, if any.
pub fn into_result(self) -> syn::Result<()> {
match self.0 {
Expand Down
43 changes: 43 additions & 0 deletions rclrs-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use proc_macro::TokenStream;

mod errors;
mod parameter_set;
mod parameter_variant;

/// Declares a struct's fields as a group of ROS 2 parameters.
///
Expand Down Expand Up @@ -54,3 +56,44 @@ pub fn derive_parameter_set(input: TokenStream) -> TokenStream {
Err(err) => err.to_compile_error().into(),
}
}

/// Represents a Rust type as a single ROS 2 parameter value.
///
/// See the `rclrs::ParameterVariant` trait for what this provides, and `rclrs::ParameterSet` for
/// declaring parameters that use it.
///
/// This is for a type you own, since a derive cannot be applied to one you do not. To use a type
/// from another crate, give the declaration a `rclrs::ParameterConversion` instead, with
/// `#[param(convert = ...)]` on a set field or `declare_parameter_with` on the builder. The
/// difference is where the representation is stated: a derive states it once for the type, a
/// conversion states it at each declaration.
///
/// The representation is chosen by the shape of the type and by `#[parameter(...)]`:
///
/// * An **enum whose variants carry no data** becomes a string holding the name of one variant.
/// `#[parameter(rename_all = "snake_case")]` sets the naming convention, one of `snake_case`,
/// `kebab-case`, `lowercase`, `UPPERCASE` or `SCREAMING_SNAKE_CASE`, and
/// `#[parameter(rename = "...")]` on a variant sets its stored value exactly. The valid values
/// appear in the parameter descriptor's constraints and in the message a rejected value gets.
/// * **`#[parameter(transparent)]`** on a type wrapping a single value gives it that value's
/// representation, including its range type. Useful for units: a `Meters(f64)` parameter
/// behaves exactly as an `f64` one, so its range is written in the units of the wrapped value,
/// `0.0..=10.0` rather than `Meters(0.0)..=Meters(10.0)`.
/// * **`#[parameter(from_str)]`** stores the type as a string, using its
/// [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display) implementations. The
/// `FromStr` error is reported when a value is rejected, so it should say what was wrong.
///
/// The type also needs to be [`Clone`], which every parameter value must be.
///
/// A derived type works with the conversion-based API as well:
/// `rclrs::ParameterConversion::of_variant` assembles a conversion from what this derive emits,
/// so the kind and the constraints carry across and `ros2 param describe` reports them either
/// way.
#[proc_macro_derive(ParameterVariant, attributes(parameter))]
pub fn derive_parameter_variant(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
match parameter_variant::expand(&input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
2 changes: 1 addition & 1 deletion rclrs-macros/src/parameter_set/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use syn::{spanned::Spanned, Attribute, Expr, ExprRange, Ident, LitStr, RangeLimits};

use super::errors::Errors;
use crate::errors::Errors;

/// Struct-level configuration from `#[parameters(...)]`.
#[derive(Default)]
Expand Down
3 changes: 1 addition & 2 deletions rclrs-macros/src/parameter_set/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@

mod attrs;
mod codegen;
mod errors;
mod known_types;

use proc_macro2::TokenStream;
use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident};

use crate::errors::Errors;
use attrs::{FieldAttrs, SetAttrs};
use errors::Errors;
use known_types::{shape_of, TypeShape};

/// One field of the struct, with everything needed to generate its declaration.
Expand Down
193 changes: 193 additions & 0 deletions rclrs-macros/src/parameter_variant/attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//! Parsing `#[parameter(...)]`.

use syn::{spanned::Spanned, Attribute, Ident, LitStr};

use crate::errors::Errors;

/// Type-level configuration from `#[parameter(...)]`.
#[derive(Default)]
pub(crate) struct TypeAttrs {
/// Represent the type as whatever the single value it wraps is represented as.
pub transparent: Option<Ident>,
/// Represent the type as a string, via `FromStr` and `Display`.
pub from_str: Option<Ident>,
/// Naming convention for the stored value of each variant.
pub rename_all: Option<RenameAll>,
}

impl TypeAttrs {
pub fn parse(attrs: &[Attribute], errors: &mut Errors) -> Self {
let mut parsed = Self::default();
for attr in attrs {
if !attr.path().is_ident("parameter") {
continue;
}
let result = attr.parse_nested_meta(|meta| {
let ident = || {
meta.path
.get_ident()
.cloned()
.unwrap_or_else(|| Ident::new("parameter", attr.path().span()))
};
if meta.path.is_ident("transparent") {
parsed.transparent = Some(ident());
} else if meta.path.is_ident("from_str") {
parsed.from_str = Some(ident());
} else if meta.path.is_ident("rename_all") {
let value: LitStr = meta.value()?.parse()?;
match RenameAll::parse(&value.value()) {
Some(convention) => parsed.rename_all = Some(convention),
None => {
return Err(syn::Error::new(
value.span(),
format!(
"unknown naming convention {:?}; expected one of {}",
value.value(),
RenameAll::NAMES.join(", "),
),
))
}
}
} else {
return Err(meta.error(
"unknown `parameter` option; expected one of `transparent`, `from_str`, \
`rename_all`",
));
}
Ok(())
});
errors.handle(result);
}
parsed
}
}

/// Variant-level configuration from `#[parameter(...)]`.
#[derive(Default)]
pub(crate) struct VariantAttrs {
/// The exact string this variant is stored as.
pub rename: Option<LitStr>,
}

impl VariantAttrs {
pub fn parse(attrs: &[Attribute], errors: &mut Errors) -> Self {
let mut parsed = Self::default();
for attr in attrs {
if !attr.path().is_ident("parameter") {
continue;
}
let result = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
parsed.rename = Some(meta.value()?.parse()?);
Ok(())
} else {
Err(meta.error("unknown `parameter` option on a variant; expected `rename`"))
}
});
errors.handle(result);
}
parsed
}
}

/// How to turn a variant's name into the string it is stored as.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RenameAll {
/// Exactly as written.
None,
SnakeCase,
KebabCase,
LowerCase,
UpperCase,
ScreamingSnakeCase,
}

impl RenameAll {
pub const NAMES: &'static [&'static str] = &[
"\"snake_case\"",
"\"kebab-case\"",
"\"lowercase\"",
"\"UPPERCASE\"",
"\"SCREAMING_SNAKE_CASE\"",
];

fn parse(value: &str) -> Option<Self> {
Some(match value {
"snake_case" => Self::SnakeCase,
"kebab-case" => Self::KebabCase,
"lowercase" => Self::LowerCase,
"UPPERCASE" => Self::UpperCase,
"SCREAMING_SNAKE_CASE" => Self::ScreamingSnakeCase,
_ => return None,
})
}

/// Applies the convention to a variant name, which is assumed to be `PascalCase`.
pub fn apply(self, name: &str) -> String {
match self {
Self::None => name.to_string(),
Self::LowerCase => name.to_lowercase(),
Self::UpperCase => name.to_uppercase(),
Self::SnakeCase => split_words(name).join("_"),
Self::KebabCase => split_words(name).join("-"),
Self::ScreamingSnakeCase => split_words(name).join("_").to_uppercase(),
}
}
}

/// Splits a `PascalCase` name into lowercase words.
///
/// A run of capitals is one word, so `HTTPServer` becomes `http_server` rather than
/// `h_t_t_p_server`.
fn split_words(name: &str) -> Vec<String> {
let chars: Vec<char> = name.chars().collect();
let mut words = Vec::new();
let mut current = String::new();
for (index, &c) in chars.iter().enumerate() {
let starts_word = c.is_uppercase()
&& index > 0
&& (
// A lowercase letter or digit before a capital ends the previous word.
!chars[index - 1].is_uppercase()
// The last capital of a run belongs to the next word: `HTTPServer`.
|| chars.get(index + 1).is_some_and(|next| next.is_lowercase())
);
if starts_word && !current.is_empty() {
words.push(std::mem::take(&mut current));
}
current.extend(c.to_lowercase());
}
if !current.is_empty() {
words.push(current);
}
words
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_naming_conventions() {
assert_eq!(RenameAll::None.apply("VelocityMode"), "VelocityMode");
assert_eq!(RenameAll::SnakeCase.apply("VelocityMode"), "velocity_mode");
assert_eq!(RenameAll::KebabCase.apply("VelocityMode"), "velocity-mode");
assert_eq!(RenameAll::LowerCase.apply("VelocityMode"), "velocitymode");
assert_eq!(RenameAll::UpperCase.apply("VelocityMode"), "VELOCITYMODE");
assert_eq!(
RenameAll::ScreamingSnakeCase.apply("VelocityMode"),
"VELOCITY_MODE"
);
}

#[test]
fn test_runs_of_capitals_are_one_word() {
assert_eq!(RenameAll::SnakeCase.apply("HTTPServer"), "http_server");
assert_eq!(RenameAll::SnakeCase.apply("PID"), "pid");
assert_eq!(
RenameAll::KebabCase.apply("UseTCPNoDelay"),
"use-tcp-no-delay"
);
assert_eq!(RenameAll::SnakeCase.apply("Velocity"), "velocity");
}
}
Loading
Loading