2025-12-17 16:19:27 +00:00
|
|
|
use std::fmt;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-02-11 10:02:49 -08:00
|
|
|
use crate::config_requirements::RequirementSource;
|
2025-12-17 16:19:27 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Error, PartialEq, Eq)]
|
2025-12-19 11:03:50 -08:00
|
|
|
pub enum ConstraintError {
|
2026-01-08 16:11:14 +00:00
|
|
|
#[error(
|
|
|
|
|
"invalid value for `{field_name}`: `{candidate}` is not in the allowed set {allowed} (set by {requirement_source})"
|
|
|
|
|
)]
|
|
|
|
|
InvalidValue {
|
|
|
|
|
field_name: &'static str,
|
|
|
|
|
candidate: String,
|
|
|
|
|
allowed: String,
|
|
|
|
|
requirement_source: RequirementSource,
|
|
|
|
|
},
|
2025-12-19 11:03:50 -08:00
|
|
|
|
|
|
|
|
#[error("field `{field_name}` cannot be empty")]
|
|
|
|
|
EmptyField { field_name: String },
|
2026-01-30 18:04:09 +00:00
|
|
|
|
|
|
|
|
#[error("invalid rules in requirements (set by {requirement_source}): {reason}")]
|
|
|
|
|
ExecPolicyParse {
|
|
|
|
|
requirement_source: RequirementSource,
|
|
|
|
|
reason: String,
|
|
|
|
|
},
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConstraintError {
|
feat: migrate to new constraint-based loading strategy (#8251)
This is a significant change to how layers of configuration are applied.
In particular, the `ConfigLayerStack` now has two important fields:
- `layers: Vec<ConfigLayerEntry>`
- `requirements: ConfigRequirements`
We merge `TomlValue`s across the layers, but they are subject to
`ConfigRequirements` before creating a `Config`.
How I would review this PR:
- start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note
the new variants added to the `ConfigLayerSource` enum:
`LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm`
- note that `ConfigLayerSource` now has a `precedence()` method and
implements `PartialOrd`
- `codex-rs/core/src/config_loader/layer_io.rs` is responsible for
loading "admin" preferences from `/etc/codex/managed_config.toml` and
MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor
of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now
include some extra information on the `LoadedConfigLayers` returned in
`layer_io.rs`.
- `codex-rs/core/src/config_loader/mod.rs` has major changes to
`load_config_layers_state()`, which is what produces `ConfigLayerStack`.
The docstring has the new specification and describes the various layers
that will be loaded and the precedence order.
- It uses the information from `LoaderOverrides` "twice," both in the
spirit of legacy support:
- We use one instances to derive an instance of `ConfigRequirements`.
Currently, the only field in `managed_config.toml` that contributes to
`ConfigRequirements` is `approval_policy`. This PR introduces
`Constrained::allow_only()` to support this.
- We use a clone of `LoaderOverrides` to derive
`ConfigLayerSource::LegacyManagedConfigTomlFromFile` and
`ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as
appropriate. As before, this ends up being a "best effort" at enterprise
controls, but is enforcement is not guaranteed like it is for
`ConfigRequirements`.
- Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists.
(Previously, a user layer was always created for `ConfigLayerStack`.)
- Similarly, we only add a "session flags" layer if there are CLI
overrides.
- `config_loader/state.rs` contains the updated implementation for
`ConfigLayerStack`. Note the public API is largely the same as before,
but the implementation is quite different. We leverage the fact that
`ConfigLayerSource` is now `PartialOrd` to ensure layers are in the
correct order.
- A `Config` constructed via `ConfigBuilder.build()` will use
`load_config_layers_state()` to create the `ConfigLayerStack` and use
the associated `ConfigRequirements` when constructing the `Config`
object.
- That said, a `Config` constructed via
`Config::load_from_base_config_with_overrides()` does _not_ yet use
`ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead
of loading a proper `ConfigRequirements`. I will fix this in a
subsequent PR.
Then the following files are mostly test changes:
```
codex-rs/app-server/tests/suite/v2/config_rpc.rs
codex-rs/core/src/config/service.rs
codex-rs/core/src/config_loader/tests.rs
```
Again, because we do not always include "user" and "session flags"
layers when the contents are empty, `ConfigLayerStack` sometimes has
fewer layers than before (and the precedence order changed slightly),
which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
|
|
|
pub fn empty_field(field_name: impl Into<String>) -> Self {
|
2025-12-19 11:03:50 -08:00
|
|
|
Self::EmptyField {
|
|
|
|
|
field_name: field_name.into(),
|
feat: migrate to new constraint-based loading strategy (#8251)
This is a significant change to how layers of configuration are applied.
In particular, the `ConfigLayerStack` now has two important fields:
- `layers: Vec<ConfigLayerEntry>`
- `requirements: ConfigRequirements`
We merge `TomlValue`s across the layers, but they are subject to
`ConfigRequirements` before creating a `Config`.
How I would review this PR:
- start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note
the new variants added to the `ConfigLayerSource` enum:
`LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm`
- note that `ConfigLayerSource` now has a `precedence()` method and
implements `PartialOrd`
- `codex-rs/core/src/config_loader/layer_io.rs` is responsible for
loading "admin" preferences from `/etc/codex/managed_config.toml` and
MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor
of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now
include some extra information on the `LoadedConfigLayers` returned in
`layer_io.rs`.
- `codex-rs/core/src/config_loader/mod.rs` has major changes to
`load_config_layers_state()`, which is what produces `ConfigLayerStack`.
The docstring has the new specification and describes the various layers
that will be loaded and the precedence order.
- It uses the information from `LoaderOverrides` "twice," both in the
spirit of legacy support:
- We use one instances to derive an instance of `ConfigRequirements`.
Currently, the only field in `managed_config.toml` that contributes to
`ConfigRequirements` is `approval_policy`. This PR introduces
`Constrained::allow_only()` to support this.
- We use a clone of `LoaderOverrides` to derive
`ConfigLayerSource::LegacyManagedConfigTomlFromFile` and
`ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as
appropriate. As before, this ends up being a "best effort" at enterprise
controls, but is enforcement is not guaranteed like it is for
`ConfigRequirements`.
- Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists.
(Previously, a user layer was always created for `ConfigLayerStack`.)
- Similarly, we only add a "session flags" layer if there are CLI
overrides.
- `config_loader/state.rs` contains the updated implementation for
`ConfigLayerStack`. Note the public API is largely the same as before,
but the implementation is quite different. We leverage the fact that
`ConfigLayerSource` is now `PartialOrd` to ensure layers are in the
correct order.
- A `Config` constructed via `ConfigBuilder.build()` will use
`load_config_layers_state()` to create the `ConfigLayerStack` and use
the associated `ConfigRequirements` when constructing the `Config`
object.
- That said, a `Config` constructed via
`Config::load_from_base_config_with_overrides()` does _not_ yet use
`ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead
of loading a proper `ConfigRequirements`. I will fix this in a
subsequent PR.
Then the following files are mostly test changes:
```
codex-rs/app-server/tests/suite/v2/config_rpc.rs
codex-rs/core/src/config/service.rs
codex-rs/core/src/config_loader/tests.rs
```
Again, because we do not always include "user" and "session flags"
layers when the contents are empty, `ConfigLayerStack` sometimes has
fewer layers than before (and the precedence order changed slightly),
which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type ConstraintResult<T> = Result<T, ConstraintError>;
|
|
|
|
|
|
|
|
|
|
impl From<ConstraintError> for std::io::Error {
|
|
|
|
|
fn from(err: ConstraintError) -> Self {
|
|
|
|
|
std::io::Error::new(std::io::ErrorKind::InvalidInput, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ConstraintValidator<T> = dyn Fn(&T) -> ConstraintResult<()> + Send + Sync;
|
2026-01-13 19:45:00 +00:00
|
|
|
/// A ConstraintNormalizer is a function which transforms a value into another of the same type.
|
|
|
|
|
/// `Constrained` uses normalizers to transform values to satisfy constraints or enforce values.
|
|
|
|
|
type ConstraintNormalizer<T> = dyn Fn(T) -> T + Send + Sync;
|
2025-12-17 16:19:27 +00:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct Constrained<T> {
|
|
|
|
|
value: T,
|
|
|
|
|
validator: Arc<ConstraintValidator<T>>,
|
2026-01-13 19:45:00 +00:00
|
|
|
normalizer: Option<Arc<ConstraintNormalizer<T>>>,
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: Send + Sync> Constrained<T> {
|
|
|
|
|
pub fn new(
|
|
|
|
|
initial_value: T,
|
|
|
|
|
validator: impl Fn(&T) -> ConstraintResult<()> + Send + Sync + 'static,
|
|
|
|
|
) -> ConstraintResult<Self> {
|
|
|
|
|
let validator: Arc<ConstraintValidator<T>> = Arc::new(validator);
|
|
|
|
|
validator(&initial_value)?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
value: initial_value,
|
|
|
|
|
validator,
|
2026-01-13 19:45:00 +00:00
|
|
|
normalizer: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// normalized creates a `Constrained` value with a normalizer function and a validator that allows any value.
|
|
|
|
|
pub fn normalized(
|
|
|
|
|
initial_value: T,
|
|
|
|
|
normalizer: impl Fn(T) -> T + Send + Sync + 'static,
|
|
|
|
|
) -> ConstraintResult<Self> {
|
|
|
|
|
let validator: Arc<ConstraintValidator<T>> = Arc::new(|_| Ok(()));
|
|
|
|
|
let normalizer: Arc<ConstraintNormalizer<T>> = Arc::new(normalizer);
|
|
|
|
|
let normalized = normalizer(initial_value);
|
|
|
|
|
validator(&normalized)?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
value: normalized,
|
|
|
|
|
validator,
|
|
|
|
|
normalizer: Some(normalizer),
|
2025-12-17 16:19:27 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn allow_any(initial_value: T) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
value: initial_value,
|
|
|
|
|
validator: Arc::new(|_| Ok(())),
|
2026-01-13 19:45:00 +00:00
|
|
|
normalizer: None,
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 15:39:22 +00:00
|
|
|
pub fn allow_only(only_value: T) -> Self
|
|
|
|
|
where
|
|
|
|
|
T: Clone + fmt::Debug + PartialEq + 'static,
|
|
|
|
|
{
|
|
|
|
|
let allowed_value = only_value.clone();
|
|
|
|
|
Self {
|
|
|
|
|
value: only_value,
|
|
|
|
|
validator: Arc::new(move |candidate| {
|
|
|
|
|
if candidate == &allowed_value {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(ConstraintError::InvalidValue {
|
|
|
|
|
field_name: "<unknown>",
|
|
|
|
|
candidate: format!("{candidate:?}"),
|
|
|
|
|
allowed: format!("[{allowed_value:?}]"),
|
|
|
|
|
requirement_source: RequirementSource::Unknown,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
normalizer: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: migrate to new constraint-based loading strategy (#8251)
This is a significant change to how layers of configuration are applied.
In particular, the `ConfigLayerStack` now has two important fields:
- `layers: Vec<ConfigLayerEntry>`
- `requirements: ConfigRequirements`
We merge `TomlValue`s across the layers, but they are subject to
`ConfigRequirements` before creating a `Config`.
How I would review this PR:
- start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note
the new variants added to the `ConfigLayerSource` enum:
`LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm`
- note that `ConfigLayerSource` now has a `precedence()` method and
implements `PartialOrd`
- `codex-rs/core/src/config_loader/layer_io.rs` is responsible for
loading "admin" preferences from `/etc/codex/managed_config.toml` and
MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor
of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now
include some extra information on the `LoadedConfigLayers` returned in
`layer_io.rs`.
- `codex-rs/core/src/config_loader/mod.rs` has major changes to
`load_config_layers_state()`, which is what produces `ConfigLayerStack`.
The docstring has the new specification and describes the various layers
that will be loaded and the precedence order.
- It uses the information from `LoaderOverrides` "twice," both in the
spirit of legacy support:
- We use one instances to derive an instance of `ConfigRequirements`.
Currently, the only field in `managed_config.toml` that contributes to
`ConfigRequirements` is `approval_policy`. This PR introduces
`Constrained::allow_only()` to support this.
- We use a clone of `LoaderOverrides` to derive
`ConfigLayerSource::LegacyManagedConfigTomlFromFile` and
`ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as
appropriate. As before, this ends up being a "best effort" at enterprise
controls, but is enforcement is not guaranteed like it is for
`ConfigRequirements`.
- Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists.
(Previously, a user layer was always created for `ConfigLayerStack`.)
- Similarly, we only add a "session flags" layer if there are CLI
overrides.
- `config_loader/state.rs` contains the updated implementation for
`ConfigLayerStack`. Note the public API is largely the same as before,
but the implementation is quite different. We leverage the fact that
`ConfigLayerSource` is now `PartialOrd` to ensure layers are in the
correct order.
- A `Config` constructed via `ConfigBuilder.build()` will use
`load_config_layers_state()` to create the `ConfigLayerStack` and use
the associated `ConfigRequirements` when constructing the `Config`
object.
- That said, a `Config` constructed via
`Config::load_from_base_config_with_overrides()` does _not_ yet use
`ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead
of loading a proper `ConfigRequirements`. I will fix this in a
subsequent PR.
Then the following files are mostly test changes:
```
codex-rs/app-server/tests/suite/v2/config_rpc.rs
codex-rs/core/src/config/service.rs
codex-rs/core/src/config_loader/tests.rs
```
Again, because we do not always include "user" and "session flags"
layers when the contents are empty, `ConfigLayerStack` sometimes has
fewer layers than before (and the precedence order changed slightly),
which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
|
|
|
/// Allow any value of T, using T's Default as the initial value.
|
|
|
|
|
pub fn allow_any_from_default() -> Self
|
|
|
|
|
where
|
|
|
|
|
T: Default,
|
|
|
|
|
{
|
|
|
|
|
Self::allow_any(T::default())
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 16:19:27 +00:00
|
|
|
pub fn get(&self) -> &T {
|
|
|
|
|
&self.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn value(&self) -> T
|
|
|
|
|
where
|
|
|
|
|
T: Copy,
|
|
|
|
|
{
|
|
|
|
|
self.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn can_set(&self, candidate: &T) -> ConstraintResult<()> {
|
|
|
|
|
(self.validator)(candidate)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set(&mut self, value: T) -> ConstraintResult<()> {
|
2026-01-13 19:45:00 +00:00
|
|
|
let value = if let Some(normalizer) = &self.normalizer {
|
|
|
|
|
normalizer(value)
|
|
|
|
|
} else {
|
|
|
|
|
value
|
|
|
|
|
};
|
2025-12-17 16:19:27 +00:00
|
|
|
(self.validator)(&value)?;
|
|
|
|
|
self.value = value;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> std::ops::Deref for Constrained<T> {
|
|
|
|
|
type Target = T;
|
|
|
|
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
|
&self.value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: fmt::Debug> fmt::Debug for Constrained<T> {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
f.debug_struct("Constrained")
|
|
|
|
|
.field("value", &self.value)
|
|
|
|
|
.finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: PartialEq> PartialEq for Constrained<T> {
|
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
|
self.value == other.value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
|
|
2026-01-08 16:11:14 +00:00
|
|
|
fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> ConstraintError {
|
|
|
|
|
ConstraintError::InvalidValue {
|
|
|
|
|
field_name: "<unknown>",
|
|
|
|
|
candidate: candidate.into(),
|
|
|
|
|
allowed: allowed.into(),
|
|
|
|
|
requirement_source: RequirementSource::Unknown,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 16:19:27 +00:00
|
|
|
#[test]
|
|
|
|
|
fn constrained_allow_any_accepts_any_value() {
|
|
|
|
|
let mut constrained = Constrained::allow_any(5);
|
|
|
|
|
constrained.set(-10).expect("allow any accepts all values");
|
|
|
|
|
assert_eq!(constrained.value(), -10);
|
|
|
|
|
}
|
|
|
|
|
|
feat: migrate to new constraint-based loading strategy (#8251)
This is a significant change to how layers of configuration are applied.
In particular, the `ConfigLayerStack` now has two important fields:
- `layers: Vec<ConfigLayerEntry>`
- `requirements: ConfigRequirements`
We merge `TomlValue`s across the layers, but they are subject to
`ConfigRequirements` before creating a `Config`.
How I would review this PR:
- start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note
the new variants added to the `ConfigLayerSource` enum:
`LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm`
- note that `ConfigLayerSource` now has a `precedence()` method and
implements `PartialOrd`
- `codex-rs/core/src/config_loader/layer_io.rs` is responsible for
loading "admin" preferences from `/etc/codex/managed_config.toml` and
MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor
of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now
include some extra information on the `LoadedConfigLayers` returned in
`layer_io.rs`.
- `codex-rs/core/src/config_loader/mod.rs` has major changes to
`load_config_layers_state()`, which is what produces `ConfigLayerStack`.
The docstring has the new specification and describes the various layers
that will be loaded and the precedence order.
- It uses the information from `LoaderOverrides` "twice," both in the
spirit of legacy support:
- We use one instances to derive an instance of `ConfigRequirements`.
Currently, the only field in `managed_config.toml` that contributes to
`ConfigRequirements` is `approval_policy`. This PR introduces
`Constrained::allow_only()` to support this.
- We use a clone of `LoaderOverrides` to derive
`ConfigLayerSource::LegacyManagedConfigTomlFromFile` and
`ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as
appropriate. As before, this ends up being a "best effort" at enterprise
controls, but is enforcement is not guaranteed like it is for
`ConfigRequirements`.
- Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists.
(Previously, a user layer was always created for `ConfigLayerStack`.)
- Similarly, we only add a "session flags" layer if there are CLI
overrides.
- `config_loader/state.rs` contains the updated implementation for
`ConfigLayerStack`. Note the public API is largely the same as before,
but the implementation is quite different. We leverage the fact that
`ConfigLayerSource` is now `PartialOrd` to ensure layers are in the
correct order.
- A `Config` constructed via `ConfigBuilder.build()` will use
`load_config_layers_state()` to create the `ConfigLayerStack` and use
the associated `ConfigRequirements` when constructing the `Config`
object.
- That said, a `Config` constructed via
`Config::load_from_base_config_with_overrides()` does _not_ yet use
`ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead
of loading a proper `ConfigRequirements`. I will fix this in a
subsequent PR.
Then the following files are mostly test changes:
```
codex-rs/app-server/tests/suite/v2/config_rpc.rs
codex-rs/core/src/config/service.rs
codex-rs/core/src/config_loader/tests.rs
```
Again, because we do not always include "user" and "session flags"
layers when the contents are empty, `ConfigLayerStack` sometimes has
fewer layers than before (and the precedence order changed slightly),
which is the main reason integration tests changed.
2025-12-18 10:06:05 -08:00
|
|
|
#[test]
|
|
|
|
|
fn constrained_allow_any_default_uses_default_value() {
|
|
|
|
|
let constrained = Constrained::<i32>::allow_any_from_default();
|
|
|
|
|
assert_eq!(constrained.value(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 19:45:00 +00:00
|
|
|
#[test]
|
2026-02-11 15:39:22 +00:00
|
|
|
fn constrained_allow_only_rejects_different_values() {
|
|
|
|
|
let mut constrained = Constrained::allow_only(5);
|
|
|
|
|
constrained
|
|
|
|
|
.set(5)
|
|
|
|
|
.expect("allowed value should be accepted");
|
|
|
|
|
|
|
|
|
|
let err = constrained
|
|
|
|
|
.set(6)
|
|
|
|
|
.expect_err("different value should be rejected");
|
|
|
|
|
assert_eq!(err, invalid_value("6", "[5]"));
|
|
|
|
|
assert_eq!(constrained.value(), 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-01-13 19:45:00 +00:00
|
|
|
fn constrained_normalizer_applies_on_init_and_set() -> anyhow::Result<()> {
|
|
|
|
|
let mut constrained = Constrained::normalized(-1, |value| value.max(0))?;
|
|
|
|
|
assert_eq!(constrained.value(), 0);
|
|
|
|
|
constrained.set(-5)?;
|
|
|
|
|
assert_eq!(constrained.value(), 0);
|
|
|
|
|
constrained.set(10)?;
|
|
|
|
|
assert_eq!(constrained.value(), 10);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 16:19:27 +00:00
|
|
|
#[test]
|
|
|
|
|
fn constrained_new_rejects_invalid_initial_value() {
|
|
|
|
|
let result = Constrained::new(0, |value| {
|
|
|
|
|
if *value > 0 {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2026-01-08 16:11:14 +00:00
|
|
|
Err(invalid_value(value.to_string(), "positive values"))
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-08 16:11:14 +00:00
|
|
|
assert_eq!(result, Err(invalid_value("0", "positive values")));
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn constrained_set_rejects_invalid_value_and_leaves_previous() {
|
|
|
|
|
let mut constrained = Constrained::new(1, |value| {
|
|
|
|
|
if *value > 0 {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2026-01-08 16:11:14 +00:00
|
|
|
Err(invalid_value(value.to_string(), "positive values"))
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.expect("initial value should be accepted");
|
|
|
|
|
|
|
|
|
|
let err = constrained
|
|
|
|
|
.set(-5)
|
|
|
|
|
.expect_err("negative values should be rejected");
|
2026-01-08 16:11:14 +00:00
|
|
|
assert_eq!(err, invalid_value("-5", "positive values"));
|
2025-12-17 16:19:27 +00:00
|
|
|
assert_eq!(constrained.value(), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn constrained_can_set_allows_probe_without_setting() {
|
|
|
|
|
let constrained = Constrained::new(1, |value| {
|
|
|
|
|
if *value > 0 {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
2026-01-08 16:11:14 +00:00
|
|
|
Err(invalid_value(value.to_string(), "positive values"))
|
2025-12-17 16:19:27 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.expect("initial value should be accepted");
|
|
|
|
|
|
|
|
|
|
constrained
|
|
|
|
|
.can_set(&2)
|
|
|
|
|
.expect("can_set should accept positive value");
|
|
|
|
|
let err = constrained
|
|
|
|
|
.can_set(&-1)
|
|
|
|
|
.expect_err("can_set should reject negative value");
|
2026-01-08 16:11:14 +00:00
|
|
|
assert_eq!(err, invalid_value("-1", "positive values"));
|
2025-12-17 16:19:27 +00:00
|
|
|
assert_eq!(constrained.value(), 1);
|
|
|
|
|
}
|
|
|
|
|
}
|