Skip to content

feat(parameters): derive descriptor constraints from the parameter type - #690

Open
azerupi wants to merge 1 commit into
azerupi/params/parameter-conversionfrom
azerupi/params/descriptor-constraints
Open

feat(parameters): derive descriptor constraints from the parameter type#690
azerupi wants to merge 1 commit into
azerupi/params/parameter-conversionfrom
azerupi/params/descriptor-constraints

Conversation

@azerupi

@azerupi azerupi commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

The code changes in this PR have been assisted by Claude Code but I have reviewed and iterated on the code.

Problem

additional_constraints in a parameter descriptor was only ever whatever the declaration passed to .constraints(). For a type with a closed set of valid values, a string-backed enum being the obvious case, that means every declaration site has to restate the type's rules as free text:

node.declare_parameter("mode")
    .default(Switch::On)
    .constraints("one of: on, off")     // restated here
    .mandatory()?;

node.declare_parameter("fallback_mode")
    .default(Switch::Off)
    .constraints("one of: on, off")     // and here
    .mandatory()?;

Nothing keeps that text in sync between the different declarations and with the type. As the type gains variants it is very easy to forget to update each declaration and that is when the user adds .constraints() to begin with. So often there would be no constraints or the constraints would be outdated and a user running ros2 param describe would not be helped.

Solution

Add a way for a type to describes its own constraints once:

impl ParameterVariant for Switch {
    fn kind() -> ParameterKind { ParameterKind::String }

    fn type_constraints() -> Option<Arc<str>> {
        Some("one of: on, off".into())
    }
}

and every declaration that sets no constraints of its own inherits it:

node.declare_parameter("mode").default(Switch::On).mandatory()?;
$ ros2 param describe /node mode
  Constraints: one of: on, off

An explicit .constraints() still wins, for a rule that belongs to one declaration rather than to the type:

.constraints("must be off on tuesdays")     // this, not the type's text

The constraint rides on the declaration's ParameterConversion (added in a prior PR), which is already where a parameter's representation is described. of_variant() fills it from T::type_constraints(), so a type with a ParameterVariant impl gets the behaviour above for free.

That placement also means a hand-written conversion can describe its values with no type to hang the rules on:

let positive_seconds = ParameterConversion::double(
        Duration::as_secs_f64,
        |v| if v > 0.0 { Duration::try_from_secs_f64(v).map_err(|e| e.to_string()) }
            else { Err("must be greater than zero".into()) },
    )
    .with_constraints("must be greater than zero");

To make this work the API grows to have

  • ParameterVariant::type_constraints() -> Option<Arc<str>> with a default implementation that defaults to None. This is purely additive and not a breaking change. Existing code continues to compile.
  • ParameterConversion::with_constraints() and ::constraints().

additional_constraints in a parameter descriptor was only ever what the
declaration passed to .constraints(). For a type with a closed set of
valid values, such as a string-backed enum, that meant every declaration
site restating the type's rules, with nothing keeping the text in sync as
variants are added -- and an empty descriptor field when the call was
forgotten, leaving operators no way to discover what a value may be.

ParameterVariant::type_constraints() lets a type describe itself once. A
declaration that sets no constraints of its own inherits it, so
`ros2 param describe` reports the valid values for free. An explicit
.constraints() still takes precedence for rules that belong to one
declaration rather than to the type.

The text is carried by the declaration's ParameterConversion, which is
where a parameter's representation is already described, so a conversion
written by hand can say what its values may be through with_constraints()
without a type to hang the rules on.

Assisted-by: Claude:claude-opus-5 [Claude Code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant