From 7796c4d4e1a9791b41430de3992363f09b45b38f Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 10 Apr 2026 07:16:10 -0400 Subject: [PATCH] refactor(settings): resolve feature flags --- lib/crates/fabro-config/src/lib.rs | 6 ++-- .../fabro-config/src/resolve/features.rs | 12 ++++++++ lib/crates/fabro-config/src/resolve/mod.rs | 18 +++++++++++- .../fabro-config/tests/resolve_features.rs | 29 +++++++++++++++++++ lib/crates/fabro-server/src/web_auth.rs | 5 ++-- .../fabro-types/src/settings/features.rs | 6 ++++ lib/crates/fabro-types/src/settings/mod.rs | 2 +- 7 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 lib/crates/fabro-config/src/resolve/features.rs create mode 100644 lib/crates/fabro-config/tests/resolve_features.rs diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index 5b5fca329..efe3d6c9a 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -15,9 +15,9 @@ pub use config::ConfigLayer; pub use fabro_util::path::expand_tilde; pub use home::Home; pub use resolve::{ - ResolveError, resolve_cli, resolve_cli_from_file, resolve_project, resolve_project_from_file, - resolve_run, resolve_run_from_file, resolve_server, resolve_server_from_file, resolve_workflow, - resolve_workflow_from_file, + ResolveError, resolve_cli, resolve_cli_from_file, resolve_features, resolve_features_from_file, + resolve_project, resolve_project_from_file, resolve_run, resolve_run_from_file, resolve_server, + resolve_server_from_file, resolve_workflow, resolve_workflow_from_file, }; pub use storage::{RunScratch, ServerState, Storage}; diff --git a/lib/crates/fabro-config/src/resolve/features.rs b/lib/crates/fabro-config/src/resolve/features.rs new file mode 100644 index 000000000..62b963044 --- /dev/null +++ b/lib/crates/fabro-config/src/resolve/features.rs @@ -0,0 +1,12 @@ +use fabro_types::settings::features::{FeaturesLayer, FeaturesSettings}; + +use super::ResolveError; + +pub fn resolve_features( + layer: &FeaturesLayer, + _errors: &mut Vec, +) -> FeaturesSettings { + FeaturesSettings { + session_sandboxes: layer.session_sandboxes.unwrap_or(false), + } +} diff --git a/lib/crates/fabro-config/src/resolve/mod.rs b/lib/crates/fabro-config/src/resolve/mod.rs index f8e1c6288..7fa33ade5 100644 --- a/lib/crates/fabro-config/src/resolve/mod.rs +++ b/lib/crates/fabro-config/src/resolve/mod.rs @@ -1,16 +1,19 @@ mod cli; mod error; +mod features; mod project; mod run; mod server; mod workflow; use fabro_types::settings::{ - CliSettings, ProjectSettings, RunSettings, ServerSettings, SettingsFile, WorkflowSettings, + CliSettings, FeaturesSettings, ProjectSettings, RunSettings, ServerSettings, SettingsFile, + WorkflowSettings, }; pub use cli::resolve_cli; pub use error::ResolveError; +pub use features::resolve_features; pub use project::resolve_project; pub use run::resolve_run; pub use server::resolve_server; @@ -51,6 +54,19 @@ pub fn resolve_project_from_file( } } +pub fn resolve_features_from_file( + file: &SettingsFile, +) -> Result> { + let mut errors = Vec::new(); + let layer = file.features.as_ref().cloned().unwrap_or_default(); + let resolved = resolve_features(&layer, &mut errors); + if errors.is_empty() { + Ok(resolved) + } else { + Err(errors) + } +} + pub fn resolve_run_from_file(file: &SettingsFile) -> Result> { let mut errors = Vec::new(); let layer = file.run.as_ref().cloned().unwrap_or_default(); diff --git a/lib/crates/fabro-config/tests/resolve_features.rs b/lib/crates/fabro-config/tests/resolve_features.rs new file mode 100644 index 000000000..9afb4853a --- /dev/null +++ b/lib/crates/fabro-config/tests/resolve_features.rs @@ -0,0 +1,29 @@ +use fabro_config::resolve_features_from_file; +use fabro_types::settings::SettingsFile; + +#[test] +fn resolves_features_defaults_from_empty_settings() { + let settings = SettingsFile::default(); + + let features = resolve_features_from_file(&settings).expect("empty settings should resolve"); + + assert!(!features.session_sandboxes); +} + +#[test] +fn resolves_session_sandboxes_flag() { + let settings: SettingsFile = fabro_config::ConfigLayer::parse( + r#" +_version = 1 + +[features] +session_sandboxes = true +"#, + ) + .expect("fixture should parse") + .into(); + + let features = resolve_features_from_file(&settings).expect("features should resolve"); + + assert!(features.session_sandboxes); +} diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index 7dc6a8f01..d2ef3f352 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -147,8 +147,9 @@ fn json_response(status: StatusCode, body: serde_json::Value) -> Response { } fn features_json(settings: &SettingsFile) -> serde_json::Value { - let features = settings.features.as_ref(); - let session_sandboxes = features.and_then(|f| f.session_sandboxes).unwrap_or(false); + let session_sandboxes = fabro_config::resolve_features_from_file(settings) + .map(|settings| settings.session_sandboxes) + .unwrap_or(false); let retros = fabro_config::resolve_run_from_file(settings) .map(|settings| settings.execution.retros) .unwrap_or(false); diff --git a/lib/crates/fabro-types/src/settings/features.rs b/lib/crates/fabro-types/src/settings/features.rs index a590076ff..f7ffe53c9 100644 --- a/lib/crates/fabro-types/src/settings/features.rs +++ b/lib/crates/fabro-types/src/settings/features.rs @@ -5,6 +5,12 @@ use serde::{Deserialize, Serialize}; +/// A structurally resolved `[features]` view for consumers. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct FeaturesSettings { + pub session_sandboxes: bool, +} + /// A sparse `[features]` layer as it appears in a single settings file. /// /// Every field is an `Option` so layers can independently set or diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 14c6369b3..0b93d3bef 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -30,7 +30,7 @@ pub use cli::{ CliUpdatesSettings, }; pub use duration::{Duration, ParseDurationError}; -pub use features::FeaturesLayer; +pub use features::{FeaturesLayer, FeaturesSettings}; pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved}; pub use model_ref::{ AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef,