lock down sparse settings exports in fabro-types

This commit is contained in:
Bryan Helmkamp 2026-04-23 19:10:45 -04:00
parent 73a47c1256
commit 1bd7b7688f
No known key found for this signature in database
17 changed files with 118 additions and 125 deletions

1
Cargo.lock generated
View file

@ -1585,6 +1585,7 @@ name = "fabro-checkpoint"
version = "0.212.0-nightly.0"
dependencies = [
"chrono",
"fabro-config",
"fabro-store",
"fabro-types",
"git2",

View file

@ -14,6 +14,7 @@ doctest = false
workspace = true
[dependencies]
fabro-config = { path = "../fabro-config" }
fabro-store = { path = "../fabro-store" }
fabro-types = { path = "../fabro-types" }
git2.workspace = true

View file

@ -1,7 +1,8 @@
use std::fmt::Write;
use fabro_config::GitAuthorLayer;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{GitAuthorLayer, GitAuthorSettings};
use fabro_types::settings::run::GitAuthorSettings;
/// Resolved git author identity for checkpoint commits.
#[derive(Debug, Clone, PartialEq)]

View file

@ -12,6 +12,7 @@ use fabro_types::settings::InterpString;
pub use features::resolve_features;
pub use project::resolve_project;
pub use run::resolve_run;
#[cfg(test)]
pub(crate) use server::dev_token_auth_enabled;
pub use server::resolve_server;
pub use workflow::resolve_workflow;

View file

@ -62,7 +62,6 @@ pub use run_id::{RunId, fixtures};
pub use run_projection::{NodeState, PendingInterviewRecord, RunProjection};
pub use run_summary::RunSummary;
pub use sandbox_record::SandboxRecord;
pub use settings::Combine;
pub use stage_id::{ParallelBranchId, StageId};
pub use start::StartRecord;
pub use status::{

View file

@ -74,7 +74,7 @@ pub struct CliLoggingSettings {
/// A sparse `[cli]` layer as it appears in a single settings file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliLayer {
pub(crate) struct CliLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<CliTargetLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -92,7 +92,7 @@ pub struct CliLayer {
/// `[cli.target]` — explicit transport selection.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")]
pub enum CliTargetLayer {
pub(crate) enum CliTargetLayer {
Http {
#[serde(default)]
url: Option<InterpString>,
@ -106,7 +106,7 @@ pub enum CliTargetLayer {
/// `[cli.auth]` — explicit auth strategy selection.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliAuthLayer {
pub(crate) struct CliAuthLayer {
/// `none` explicitly disables inherited auth.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strategy: Option<CliAuthStrategy>,
@ -122,7 +122,7 @@ pub enum CliAuthStrategy {
/// `[cli.exec]` — `fabro exec` defaults.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliExecLayer {
pub(crate) struct CliExecLayer {
/// Prevent idle sleep on macOS while an exec run is in flight.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prevent_idle_sleep: Option<bool>,
@ -134,7 +134,7 @@ pub struct CliExecLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliExecModelLayer {
pub(crate) struct CliExecModelLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -143,7 +143,7 @@ pub struct CliExecModelLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliExecAgentLayer {
pub(crate) struct CliExecAgentLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permissions: Option<AgentPermissions>,
/// Agent-scoped MCP entries for `fabro exec`.
@ -154,7 +154,7 @@ pub struct CliExecAgentLayer {
/// `[cli.output]` — generic CLI output defaults.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliOutputLayer {
pub(crate) struct CliOutputLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<OutputFormat>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -181,7 +181,7 @@ pub enum OutputVerbosity {
/// `[cli.updates]` — upgrade check toggle.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliUpdatesLayer {
pub(crate) struct CliUpdatesLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub check: Option<bool>,
}
@ -189,7 +189,7 @@ pub struct CliUpdatesLayer {
/// `[cli.logging]` — process-owned logging configuration for the CLI.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CliLoggingLayer {
pub(crate) struct CliLoggingLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<String>,
}

View file

@ -19,7 +19,7 @@ use super::server::{
};
use super::size::Size;
pub trait Combine {
pub(crate) trait Combine {
/// Combine two values, preferring the values in `self`.
#[must_use]
fn combine(self, other: Self) -> Self;
@ -137,7 +137,7 @@ impl Combine for RunCheckpointLayer {
/// An element of a splice-aware sequence: either a regular value or the
/// `...` marker that asks the combiner to expand the fallback list inline.
pub trait SpliceMarker {
pub(crate) trait SpliceMarker {
fn is_splice(&self) -> bool;
}
@ -217,12 +217,21 @@ fn combine_hooks(fallback: &[HookEntry], current: Vec<HookEntry>) -> Vec<HookEnt
mod tests {
use super::*;
#[derive(Debug, PartialEq, fabro_macros::Combine)]
#[derive(Debug, PartialEq)]
struct FieldMergeLayer {
a: Option<u32>,
b: Option<u32>,
}
impl Combine for FieldMergeLayer {
fn combine(self, other: Self) -> Self {
Self {
a: self.a.combine(other.a),
b: self.b.combine(other.b),
}
}
}
#[derive(Debug, PartialEq)]
struct WholeReplaceLayer {
a: Option<u32>,

View file

@ -17,7 +17,7 @@ pub struct FeaturesNamespace {
/// override a flag without forcing a default that hides an unset value.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeaturesLayer {
pub(crate) struct FeaturesLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_sandboxes: Option<bool>,
}

View file

@ -16,7 +16,7 @@ use super::workflow::WorkflowLayer;
/// A sparse settings layer before merge/resolve.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SettingsLayer {
pub(crate) struct SettingsLayer {
#[serde(default, rename = "_version", skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -39,7 +39,7 @@ impl SettingsLayer {
/// with `["dev-token"]`. Use anywhere a test needs a starter
/// `SettingsLayer` that the strict resolver will accept.
#[must_use]
pub fn test_default() -> Self {
pub(crate) fn test_default() -> Self {
let mut layer = Self::default();
layer.ensure_test_auth_methods();
layer
@ -48,7 +48,7 @@ impl SettingsLayer {
/// If `server.auth.methods` is unset, populate it with `["dev-token"]`.
/// Existing methods (set by a fixture) are preserved. Use to make a
/// parsed-from-TOML layer resolve cleanly without overriding test intent.
pub fn ensure_test_auth_methods(&mut self) {
pub(crate) fn ensure_test_auth_methods(&mut self) {
use super::server::{ServerAuthLayer, ServerAuthMethod, ServerLayer as ServerLayerTy};
if self

View file

@ -8,26 +8,26 @@ use super::combine::Combine;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ReplaceMap<V>(pub HashMap<String, V>);
pub(crate) struct ReplaceMap<V>(pub HashMap<String, V>);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct StickyMap<V>(pub HashMap<String, V>);
pub(crate) struct StickyMap<V>(pub HashMap<String, V>);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MergeMap<V>(pub HashMap<String, V>);
pub(crate) struct MergeMap<V>(pub HashMap<String, V>);
macro_rules! impl_map_wrapper {
($name:ident) => {
impl<V> $name<V> {
#[must_use]
pub fn is_empty(&self) -> bool {
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn into_inner(self) -> HashMap<String, V> {
pub(crate) fn into_inner(self) -> HashMap<String, V> {
self.0
}
}
@ -107,12 +107,21 @@ impl<V: Combine> Combine for MergeMap<V> {
mod tests {
use super::*;
#[derive(Debug, PartialEq, fabro_macros::Combine)]
#[derive(Debug, PartialEq)]
struct ValueLayer {
a: Option<String>,
b: Option<String>,
}
impl Combine for ValueLayer {
fn combine(self, other: Self) -> Self {
Self {
a: self.a.combine(other.a),
b: self.b.combine(other.b),
}
}
}
#[test]
fn replace_map_self_wins_when_non_empty() {
let this = ReplaceMap(HashMap::from([("a".to_string(), "this".to_string())]));

View file

@ -10,51 +10,46 @@
//! exists.
pub mod cli;
pub mod combine;
mod combine;
pub mod duration;
pub mod features;
pub mod interp;
pub mod layer;
pub mod maps;
mod layer;
mod maps;
pub mod model_ref;
pub mod project;
pub mod run;
pub mod server;
pub mod size;
pub mod splice_array;
mod splice_array;
pub mod workflow;
pub use cli::{
CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings, CliLayer,
CliAuthSettings, CliExecAgentSettings, CliExecModelSettings, CliExecSettings,
CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetSettings, CliUpdatesSettings,
};
pub use combine::Combine;
pub use duration::{Duration, ParseDurationError};
pub use features::{FeaturesLayer, FeaturesNamespace};
pub use features::FeaturesNamespace;
pub use interp::{InterpString, Provenance, ResolveEnvError, Resolved};
pub use layer::SettingsLayer;
pub use maps::{MergeMap, ReplaceMap, StickyMap};
pub use model_ref::{
AmbiguousModelRef, ModelRef, ModelRegistry, ParseModelRefError, ResolvedModelRef,
};
pub use project::{ProjectLayer, ProjectNamespace};
pub use project::ProjectNamespace;
pub use run::{
ArtifactsSettings, DaytonaSettings, DaytonaSnapshotSettings, DockerfileSource,
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings,
RunAgentSettings, RunCheckpointSettings, RunExecutionSettings, RunGitSettings, RunGoal,
RunInterviewsSettings, RunLayer, RunModelSettings, RunNamespace, RunPrepareSettings,
RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
RunInterviewsSettings, RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings,
RunScmSettings, ScmGitHubSettings, TlsMode,
};
pub use server::{
DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings,
IpAllowEntry, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,
ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,
ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerIpAllowlistOverrideSettings,
ServerIpAllowlistSettings, ServerLayer, ServerListenSettings, ServerLoggingSettings,
ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings,
ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerListenSettings,
ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings,
ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
};
pub use size::{ParseSizeError, Size};
pub use splice_array::{SPLICE_MARKER, SpliceArray, SpliceArrayError};
pub use workflow::{WorkflowLayer, WorkflowNamespace};
pub use workflow::WorkflowNamespace;

View file

@ -21,7 +21,7 @@ pub struct ProjectNamespace {
/// A sparse `[project]` layer as it appears in a single settings file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectLayer {
pub(crate) struct ProjectLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]

View file

@ -429,7 +429,7 @@ pub struct ArtifactsSettings {
/// A sparse `[run]` layer as it appears in a single settings file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunLayer {
pub(crate) struct RunLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal: Option<RunGoalLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -491,7 +491,7 @@ pub struct RunLayer {
/// effective working directory.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum RunGoalLayer {
pub(crate) enum RunGoalLayer {
Inline(InterpString),
File { file: InterpString },
}
@ -520,7 +520,7 @@ pub enum ResolvedGoalSource {
/// `[run.model]` — provider-neutral default model selection.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunModelLayer {
pub(crate) struct RunModelLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -533,7 +533,7 @@ pub struct RunModelLayer {
/// A single `fallbacks` entry: either a parsed `ModelRef` or the splice marker.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelRefOrSplice {
pub(crate) enum ModelRefOrSplice {
ModelRef(ModelRef),
Splice,
}
@ -562,14 +562,14 @@ impl<'de> Deserialize<'de> for ModelRefOrSplice {
/// `[run.git]` — local git behavior such as commit author.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunGitLayer {
pub(crate) struct RunGitLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author: Option<GitAuthorLayer>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GitAuthorLayer {
pub(crate) struct GitAuthorLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -580,7 +580,7 @@ pub struct GitAuthorLayer {
/// across layers.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunPrepareLayer {
pub(crate) struct RunPrepareLayer {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub steps: Vec<PrepareStep>,
/// Optional timeout applied to each prepare step.
@ -603,7 +603,7 @@ pub struct PrepareStep {
/// `[run.execution]` — run posture knobs.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunExecutionLayer {
pub(crate) struct RunExecutionLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<RunMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -630,7 +630,7 @@ pub enum ApprovalMode {
/// `[run.checkpoint]` — checkpoint policy.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunCheckpointLayer {
pub(crate) struct RunCheckpointLayer {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude_globs: Vec<String>,
}
@ -638,7 +638,7 @@ pub struct RunCheckpointLayer {
/// `[run.sandbox]` — sandbox selection and execution-environment surface.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunSandboxLayer {
pub(crate) struct RunSandboxLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -656,7 +656,7 @@ pub struct RunSandboxLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalSandboxLayer {
pub(crate) struct LocalSandboxLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree_mode: Option<WorktreeMode>,
}
@ -673,7 +673,7 @@ pub enum WorktreeMode {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DaytonaSandboxLayer {
pub(crate) struct DaytonaSandboxLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_stop_interval: Option<i32>,
/// Sticky merge-by-key (provider-native labels).
@ -689,7 +689,7 @@ pub struct DaytonaSandboxLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DaytonaSnapshotLayer {
pub(crate) struct DaytonaSnapshotLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -704,7 +704,7 @@ pub struct DaytonaSnapshotLayer {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum DaytonaDockerfileLayer {
pub(crate) enum DaytonaDockerfileLayer {
Inline(String),
Path { path: String },
}
@ -720,7 +720,7 @@ pub enum DaytonaNetworkLayer {
/// `[run.notifications.<name>]` — a keyed notification route.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NotificationRouteLayer {
pub(crate) struct NotificationRouteLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -739,7 +739,7 @@ pub struct NotificationRouteLayer {
/// A single string array entry that may be the splice marker.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StringOrSplice {
pub(crate) enum StringOrSplice {
Value(String),
Splice,
}
@ -767,7 +767,7 @@ impl<'de> Deserialize<'de> for StringOrSplice {
/// Provider-specific destination fields for a notification route.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NotificationProviderLayer {
pub(crate) struct NotificationProviderLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<InterpString>,
}
@ -775,7 +775,7 @@ pub struct NotificationProviderLayer {
/// `[run.interviews]` — external interview delivery.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InterviewsLayer {
pub(crate) struct InterviewsLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -788,7 +788,7 @@ pub struct InterviewsLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InterviewProviderLayer {
pub(crate) struct InterviewProviderLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<InterpString>,
}
@ -796,7 +796,7 @@ pub struct InterviewProviderLayer {
/// `[run.agent]` — agent knobs only (permissions, MCPs).
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunAgentLayer {
pub(crate) struct RunAgentLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permissions: Option<AgentPermissions>,
/// Agent-scoped MCP server entries, keyed by name.
@ -817,7 +817,7 @@ pub enum AgentPermissions {
/// transports use neither field.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")]
pub enum McpEntryLayer {
pub(crate) enum McpEntryLayer {
Http {
#[serde(default)]
enabled: Option<bool>,
@ -865,7 +865,7 @@ pub enum McpEntryLayer {
/// used for cross-layer replace-by-id merging.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HookEntry {
pub(crate) struct HookEntry {
/// Optional merge identity. Hooks with the same `id` replace in place.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
@ -906,7 +906,7 @@ pub struct HookEntry {
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookTlsMode {
pub(crate) enum HookTlsMode {
#[default]
Verify,
NoVerify,
@ -918,7 +918,7 @@ pub enum HookTlsMode {
/// struct without a discriminator.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookAgentMarker {
pub(crate) enum HookAgentMarker {
#[default]
Enabled,
}
@ -947,7 +947,7 @@ pub enum HookEvent {
/// `[run.scm]` — remote SCM host/provider behavior.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunScmLayer {
pub(crate) struct RunScmLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -964,12 +964,12 @@ pub struct RunScmLayer {
/// `run.pull_request` until a concrete use case lands.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScmGitHubLayer;
pub(crate) struct ScmGitHubLayer;
/// `[run.pull_request]` — provider-neutral PR behavior.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunPullRequestLayer {
pub(crate) struct RunPullRequestLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -991,7 +991,7 @@ pub enum MergeStrategy {
/// `[run.artifacts]` — run artifact collection policy.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunArtifactsLayer {
pub(crate) struct RunArtifactsLayer {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub include: Vec<String>,
}

View file

@ -309,7 +309,7 @@ where
/// A sparse `[server]` layer as it appears in a single settings file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerLayer {
pub(crate) struct ServerLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub listen: Option<ServerListenLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -337,7 +337,7 @@ pub struct ServerLayer {
/// `[server.listen]` — shared bind transport.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")]
pub enum ServerListenLayer {
pub(crate) enum ServerListenLayer {
Tcp {
#[serde(default)]
address: Option<InterpString>,
@ -353,7 +353,7 @@ pub enum ServerListenLayer {
/// `url` is an optional public URL; it is **not** derived from `server.listen`.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerApiLayer {
pub(crate) struct ServerApiLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<InterpString>,
}
@ -361,7 +361,7 @@ pub struct ServerApiLayer {
/// `[server.web]` — web surface settings.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerWebLayer {
pub(crate) struct ServerWebLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -375,7 +375,7 @@ pub struct ServerWebLayer {
/// explicitly opt in to insecure configurations.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerAuthLayer {
pub(crate) struct ServerAuthLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub methods: Option<Vec<ServerAuthMethod>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -384,14 +384,14 @@ pub struct ServerAuthLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerAuthGithubLayer {
pub(crate) struct ServerAuthGithubLayer {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_usernames: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerIpAllowlistLayer {
pub(crate) struct ServerIpAllowlistLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entries: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -400,7 +400,7 @@ pub struct ServerIpAllowlistLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerIpAllowlistOverrideLayer {
pub(crate) struct ServerIpAllowlistOverrideLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entries: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -410,7 +410,7 @@ pub struct ServerIpAllowlistOverrideLayer {
/// `[server.storage]` — single managed local disk root.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerStorageLayer {
pub(crate) struct ServerStorageLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<InterpString>,
}
@ -418,7 +418,7 @@ pub struct ServerStorageLayer {
/// `[server.artifacts]` — object-store-backed artifact storage.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerArtifactsLayer {
pub(crate) struct ServerArtifactsLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -432,7 +432,7 @@ pub struct ServerArtifactsLayer {
/// `[server.slatedb]` — SlateDB bottomless storage plus tunables.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerSlateDbLayer {
pub(crate) struct ServerSlateDbLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -458,7 +458,7 @@ pub enum ObjectStoreProvider {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreLocalLayer {
pub(crate) struct ObjectStoreLocalLayer {
/// Overrides the default root, which otherwise falls back to
/// `{server.storage.root}/objects/{domain}`.
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -467,7 +467,7 @@ pub struct ObjectStoreLocalLayer {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreS3Layer {
pub(crate) struct ObjectStoreS3Layer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bucket: Option<InterpString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -481,7 +481,7 @@ pub struct ObjectStoreS3Layer {
/// `[server.scheduler]` — server-managed execution policy.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerSchedulerLayer {
pub(crate) struct ServerSchedulerLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_concurrent_runs: Option<usize>,
}
@ -489,7 +489,7 @@ pub struct ServerSchedulerLayer {
/// `[server.logging]` — process-owned logging configuration for the server.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerLoggingLayer {
pub(crate) struct ServerLoggingLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<String>,
}
@ -500,7 +500,7 @@ pub struct ServerLoggingLayer {
/// shape so strict unknown-field validation still holds.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerIntegrationsLayer {
pub(crate) struct ServerIntegrationsLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub github: Option<GithubIntegrationLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -515,7 +515,7 @@ pub struct ServerIntegrationsLayer {
/// webhooks.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GithubIntegrationLayer {
pub(crate) struct GithubIntegrationLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -535,7 +535,7 @@ pub struct GithubIntegrationLayer {
/// `[server.integrations.slack]` — Slack workspace credentials and defaults.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SlackIntegrationLayer {
pub(crate) struct SlackIntegrationLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -545,7 +545,7 @@ pub struct SlackIntegrationLayer {
/// `[server.integrations.discord]` — Discord workspace configuration.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DiscordIntegrationLayer {
pub(crate) struct DiscordIntegrationLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
@ -553,14 +553,14 @@ pub struct DiscordIntegrationLayer {
/// `[server.integrations.teams]` — Microsoft Teams configuration.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TeamsIntegrationLayer {
pub(crate) struct TeamsIntegrationLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntegrationWebhooksLayer {
pub(crate) struct IntegrationWebhooksLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strategy: Option<WebhookStrategy>,
#[serde(default, skip_serializing_if = "Option::is_none")]

View file

@ -14,11 +14,11 @@ use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// The reserved literal that marks the splice insertion point.
pub const SPLICE_MARKER: &str = "...";
pub(crate) const SPLICE_MARKER: &str = "...";
/// A string array that may contain at most one splice marker.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SpliceArray {
pub(crate) struct SpliceArray {
entries: Vec<Entry>,
}
@ -30,7 +30,7 @@ enum Entry {
/// An error returned when a splice array fails validation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpliceArrayError {
pub(crate) enum SpliceArrayError {
/// The array contained more than one splice marker.
MultipleMarkers,
}
@ -49,7 +49,7 @@ impl std::error::Error for SpliceArrayError {}
impl SpliceArray {
/// Build a splice array from a raw `Vec<String>`.
pub fn from_raw(raw: Vec<String>) -> Result<Self, SpliceArrayError> {
pub(crate) fn from_raw(raw: Vec<String>) -> Result<Self, SpliceArrayError> {
let mut entries = Vec::with_capacity(raw.len());
let mut marker_count = 0;
for item in raw {
@ -68,7 +68,7 @@ impl SpliceArray {
/// Build a splice array with no inherited splice marker.
#[must_use]
pub fn from_values(values: impl IntoIterator<Item = String>) -> Self {
pub(crate) fn from_values(values: impl IntoIterator<Item = String>) -> Self {
Self {
entries: values.into_iter().map(Entry::Value).collect(),
}
@ -76,19 +76,19 @@ impl SpliceArray {
/// True when the array contains a splice marker.
#[must_use]
pub fn has_splice(&self) -> bool {
pub(crate) fn has_splice(&self) -> bool {
self.entries.iter().any(|e| matches!(e, Entry::Splice))
}
/// The index of the splice marker, if present.
#[must_use]
pub fn splice_position(&self) -> Option<usize> {
pub(crate) fn splice_position(&self) -> Option<usize> {
self.entries.iter().position(|e| matches!(e, Entry::Splice))
}
/// The non-splice values, in source order.
#[must_use]
pub fn values(&self) -> Vec<&str> {
pub(crate) fn values(&self) -> Vec<&str> {
self.entries
.iter()
.filter_map(|e| match e {
@ -105,7 +105,7 @@ impl SpliceArray {
/// - If the array has no splice marker, it replaces the inherited list
/// wholesale.
#[must_use]
pub fn resolve(self, inherited: Vec<String>) -> Vec<String> {
pub(crate) fn resolve(self, inherited: Vec<String>) -> Vec<String> {
let Some(pos) = self.splice_position() else {
return self
.entries

View file

@ -21,7 +21,7 @@ pub struct WorkflowNamespace {
/// A sparse `[workflow]` layer as it appears in a single settings file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkflowLayer {
pub(crate) struct WorkflowLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]

View file

@ -1,23 +0,0 @@
use fabro_types::settings::SettingsLayer;
use serde_json::json;
#[test]
fn settings_layer_round_trips_github_integration_strategy() {
let source = json!({
"_version": 1,
"server": {
"integrations": {
"github": {
"strategy": "token",
"app_id": "{{ env.GITHUB_APP_ID }}"
}
}
}
});
let settings: SettingsLayer =
serde_json::from_value(source.clone()).expect("settings should deserialize");
let round_trip = serde_json::to_value(&settings).expect("settings should serialize");
assert_eq!(round_trip, source);
}