From 08c8c1179855ef05533b612eab0f703b57591c67 Mon Sep 17 00:00:00 2001 From: webdevtodayjason <115739170+webdevtodayjason@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:12:21 +0000 Subject: [PATCH] Plug-ins: numeric defaults stay in their declared range --- crates/plugins/src/manifest.rs | 30 +++++++++++++++++++++++++----- docs/plugins.md | 3 ++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/plugins/src/manifest.rs b/crates/plugins/src/manifest.rs index a9d2e34c..d74458ca 100644 --- a/crates/plugins/src/manifest.rs +++ b/crates/plugins/src/manifest.rs @@ -67,11 +67,11 @@ fn int_max() -> i64 { } impl ParamSpec { - /// The value a parameter takes when it isn't given. + /// The value a parameter takes when it isn't given (numeric defaults clamped into range). pub fn default_value(&self) -> Value { match self { - ParamSpec::Number { default, .. } => json!(default), - ParamSpec::Int { default, .. } => json!(default), + ParamSpec::Number { min, max, default } => json!(default.clamp(*min, *max)), + ParamSpec::Int { min, max, default } => json!((*default).clamp(*min, *max)), ParamSpec::Bool { default } => json!(default), ParamSpec::Choice { options, default } => json!(default.clone().or_else(|| options.first().cloned()).unwrap_or_default()), } @@ -167,8 +167,8 @@ impl Manifest { .params .iter() .map(|(k, spec)| match spec { - ParamSpec::Number { min, max, default } => format!("{k}: {min}..{max} ({default})"), - ParamSpec::Int { min, max, default } => format!("{k}: int {min}..{max} ({default})"), + ParamSpec::Number { min, max, default } => format!("{k}: {min}..{max} ({})", default.clamp(*min, *max)), + ParamSpec::Int { min, max, default } => format!("{k}: int {min}..{max} ({})", (*default).clamp(*min, *max)), ParamSpec::Bool { default } => format!("{k}: bool ({default})"), ParamSpec::Choice { options, .. } => { let opts: Vec = options.iter().map(|o| format!("{o:?}")).collect(); @@ -335,6 +335,26 @@ mod tests { } } + #[test] + fn numeric_defaults_stay_in_the_declared_range() { + let m = Manifest::parse( + br#"{"id":"org.example.defaults","name":"Defaults","kind":"filter", + "params":{"count":{"type":"int","min":1,"max":10}, + "seed":{"type":"int","min":-9007199254740995,"max":-9007199254740993,"default":0}, + "amount":{"type":"number","min":0,"max":100,"default":500}}}"#, + ) + .unwrap(); + let expected = json!({"count": 1, "seed": -9007199254740993_i64, "amount": 100.0}); + assert_eq!(Value::Object(m.defaults()), expected); + assert_eq!( + m.params_doc(), + "{count: int 1..10 (1), seed: int -9007199254740995..-9007199254740993 (-9007199254740993), amount: 0..100 (100)}" + ); + for given in [Value::Null, json!({}), json!({"count": null, "seed": null, "amount": null})] { + assert_eq!(Value::Object(m.resolve_params(&given).unwrap()), expected); + } + } + #[test] fn rejects_bad_manifests() { for bad in [ diff --git a/docs/plugins.md b/docs/plugins.md index 1c45e22a..cd672f57 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -111,7 +111,8 @@ pointers are offsets into the module's memory. - `name`: the menu label (1–64 printable characters). `kind`: `"filter"` (object filter) or `"effect"` (live effect). - `params`: in dialog order. Names are `[A-Za-z0-9_]`, not starting with `_`, and not `id`, `ids`, `params` or `preview`. The host validates the user's values before calling: missing values take the default, numbers are - clamped to `min..max` (ints rounded), a wrong type or an unknown choice is an error. + clamped to `min..max` (ints rounded), a wrong type or an unknown choice is an error. Numeric defaults + are also clamped to their range, including the implicit zero default of an `int` parameter. ### Input