radicle: Introduce `radicle::node::config::Scope`

The `SeedingPolicy` type now requires a `Scope`, however existing
configurations allow it to not be specified.

Introduce a `radicle::node::config::Scope` type that allows for an
optional `policy::Scope`. The default value resolves to
`policy::Scope::All`. This preserves backwards compatibility for
existing configurations.
This commit is contained in:
Adrian Duke 2026-02-05 14:44:19 +00:00 committed by Lorenz Leutgeb
parent 306d0afbfe
commit cee3659ed5
3 changed files with 154 additions and 6 deletions

View File

@ -72,6 +72,7 @@ pretty_assertions = { workspace = true }
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
radicle-cob = { workspace = true, features = ["stable-commit-ids", "test"] }
radicle-core = {workspace = true, features = ["qcheck"] }
radicle-crypto = { workspace = true, features = ["test"] }
radicle-git-metadata = { workspace = true }
tempfile = { workspace = true }

View File

@ -9,9 +9,11 @@ use serde::{Deserialize, Serialize};
use serde_json as json;
use crate::node;
use crate::node::policy::{Scope, SeedingPolicy};
use crate::node::policy::SeedingPolicy;
use crate::node::{Address, Alias, NodeId};
use super::policy;
/// Peer-to-peer protocol version.
pub type ProtocolVersion = u8;
@ -364,13 +366,13 @@ pub enum AddressConfig {
/// Default seeding policy. Applies when no repository policies for the given repo are found.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "default")]
#[serde(tag = "default", rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DefaultSeedingPolicy {
/// Allow seeding.
Allow {
/// Seeding scope.
#[serde(default)]
#[serde(skip_serializing_if = "Scope::is_implicit")]
scope: Scope,
},
/// Block seeding.
@ -378,6 +380,53 @@ pub enum DefaultSeedingPolicy {
Block,
}
/// [`Scope`] provides a schema for [`policy::Scope`], where the inner scope is
/// optional. It is introduced to allow ease migration to a future
/// version of [`DefaultSeedingPolicy::Allow`], where no or different defaults
/// apply to [`DefaultSeedingPolicy::Allow::scope`].
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct Scope(Option<policy::Scope>);
impl Scope {
/// Construct the implicit scope, where the default value,
/// [`policy::Scope::All`], is chosen for the final scope value.
pub fn implicit() -> Self {
Self(None)
}
/// Construct the explicit scope, where the given [`policy::Scope`] is used.
pub fn explicit(scope: policy::Scope) -> Self {
Self(Some(scope))
}
/// Resolve this [`Scope`] to its [`policy::Scope`] value.
///
/// If the scope is implicit, then [`policy::Scope::All`] is returned.
pub fn into_inner(self) -> policy::Scope {
self.0.unwrap_or(policy::Scope::All)
}
/// Returns `true` when the scope is implicit, i.e. no [`policy::Scope`] was
/// given.
pub fn is_implicit(&self) -> bool {
self.0.is_none()
}
/// Construct the explicit [`Scope`] where the inner scope is
/// [`policy::Scope::All`].
fn all() -> Self {
Self::explicit(policy::Scope::All)
}
/// Construct the explicit [`Scope`] where the inner scope is
/// [`policy::Scope::Followed`].
fn followed() -> Self {
Self::explicit(policy::Scope::Followed)
}
}
impl DefaultSeedingPolicy {
/// Is this an "allow" policy.
pub fn is_allow(&self) -> bool {
@ -386,7 +435,16 @@ impl DefaultSeedingPolicy {
/// Seed everything from anyone.
pub fn permissive() -> Self {
Self::Allow { scope: Scope::All }
Self::Allow {
scope: Scope::all(),
}
}
/// Seed only delegate changes.
pub fn followed() -> Self {
Self::Allow {
scope: Scope::followed(),
}
}
}
@ -394,7 +452,9 @@ impl From<DefaultSeedingPolicy> for SeedingPolicy {
fn from(policy: DefaultSeedingPolicy) -> Self {
match policy {
DefaultSeedingPolicy::Block => Self::Block,
DefaultSeedingPolicy::Allow { scope } => Self::Allow { scope },
DefaultSeedingPolicy::Allow { scope } => SeedingPolicy::Allow {
scope: scope.into_inner(),
},
}
}
}
@ -646,6 +706,10 @@ wrapper!(
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::{DefaultSeedingPolicy, Scope};
use crate::node::policy;
use serde_json::json;
#[test]
fn partial() {
use super::Config;
@ -683,4 +747,85 @@ mod test {
);
assert_eq!(config.limits.connection.outbound.0, 1337);
}
#[test]
fn deserialize_migrating_scope() {
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow"
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow { scope: Scope(None) }
);
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow",
"scope": null
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow { scope: Scope(None) }
);
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow",
"scope": "all"
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::All))
}
);
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow",
"scope": "followed"
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::Followed))
}
)
}
#[test]
fn serialize_migrating_scope() {
assert_eq!(
json!({
"default": "allow"
}),
serde_json::to_value(DefaultSeedingPolicy::Allow { scope: Scope(None) }).unwrap()
);
assert_eq!(
json!({
"default": "allow",
"scope": "all"
}),
serde_json::to_value(DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::All))
})
.unwrap()
);
assert_eq!(
json!({
"default": "allow",
"scope": "followed"
}),
serde_json::to_value(DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::Followed))
})
.unwrap()
);
}
}

View File

@ -192,7 +192,9 @@ impl Config {
) {
log::warn!(target: "radicle", "Overwriting `seedingPolicy` configuration");
cfg.node.seeding_policy = match policy {
Policy::Allow => DefaultSeedingPolicy::Allow { scope },
Policy::Allow => DefaultSeedingPolicy::Allow {
scope: node::config::Scope::explicit(scope),
},
Policy::Block => DefaultSeedingPolicy::Block,
}
}