From a8b7bcc63586e227730374108d93302a99de992e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 7 May 2026 15:45:42 -0400 Subject: [PATCH] Stop Saga's from leaking when feature disabled --- AGENTS.md | 2 +- v-api-permission-derive/Cargo.toml | 4 ++++ v-api-permission-derive/src/lib.rs | 28 ++++++++++++++++++----- v-api/Cargo.toml | 2 +- v-api/src/context/mod.rs | 36 +++++++++++++++++------------- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 00efd7d..5ad7ade 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ - check: `cargo check --quiet --all-features --workspace --all-targets` - test: `cargo test --quiet --all-features --workspace --all-targets` - format: `cargo fmt` -- clippy: `cargo clippy --fix --allow-dirty --all-features --workspace --all-targets` +- clippy: `cargo clippy --quiet --fix --allow-dirty --all-features --workspace --all-targets` ## Crates diff --git a/v-api-permission-derive/Cargo.toml b/v-api-permission-derive/Cargo.toml index 15f4b49..8d38e16 100644 --- a/v-api-permission-derive/Cargo.toml +++ b/v-api-permission-derive/Cargo.toml @@ -7,6 +7,10 @@ publish.workspace = true [lib] proc-macro = true +[features] +default = [] +sagas = [] + [dependencies] heck = { workspace = true } proc-macro2 = { workspace = true } diff --git a/v-api-permission-derive/src/lib.rs b/v-api-permission-derive/src/lib.rs index 3abb41d..816ea14 100644 --- a/v-api-permission-derive/src/lib.rs +++ b/v-api-permission-derive/src/lib.rs @@ -366,6 +366,15 @@ fn from_system_permission_tokens( source: &Ident, permission_type: &Ident, ) -> proc_macro2::TokenStream { + let saga_permission_from_tokens = if cfg!(feature = "sagas") { + quote! { + VPermission::GetSagasAll => Self::GetSagasAll, + VPermission::ManageSagasAll => Self::ManageSagasAll, + } + } else { + quote! {} + }; + if source != permission_type { quote! { impl From<#source> for #permission_type { @@ -440,8 +449,7 @@ fn from_system_permission_tokens( VPermission::CreateAccessToken => Self::CreateAccessToken, - VPermission::GetSagasAll => Self::GetSagasAll, - VPermission::ManageSagasAll => Self::ManageSagasAll, + #saga_permission_from_tokens VPermission::Unsupported(inner) => Self::Unsupported(inner), } @@ -473,6 +481,17 @@ fn inject_system_permission_variants(mut input: DeriveInput) -> Result TokenStream { + let saga_permission_tokens = if cfg!(feature = "sagas") { + quote! { + #[v_api(scope(to = "saga:r", from = "saga:r"))] + GetSagasAll, + #[v_api(scope(to = "saga:w", from = "saga:w"))] + ManageSagasAll, + } + } else { + quote! {} + }; + quote! { enum VPermission { #[v_api(scope(to = "user:info:w", from = "user:info:w"))] @@ -723,10 +742,7 @@ fn system_permission_tokens() -> TokenStream { CreateAccessToken, - #[v_api(scope(to = "saga:r", from = "saga:r"))] - GetSagasAll, - #[v_api(scope(to = "saga:w", from = "saga:w"))] - ManageSagasAll, + #saga_permission_tokens #[serde(untagged)] Unsupported(serde_json::Value), diff --git a/v-api/Cargo.toml b/v-api/Cargo.toml index 35e0a58..7c843b4 100644 --- a/v-api/Cargo.toml +++ b/v-api/Cargo.toml @@ -7,7 +7,7 @@ publish.workspace = true [features] default = ["sagas"] local-dev = [] -sagas = ["slog", "steno", "v-model/sagas"] +sagas = ["slog", "steno", "v-api-permission-derive/sagas", "v-model/sagas"] [dependencies] anyhow = { workspace = true } diff --git a/v-api/src/context/mod.rs b/v-api/src/context/mod.rs index e95b2b2..94f8754 100644 --- a/v-api/src/context/mod.rs +++ b/v-api/src/context/mod.rs @@ -1211,6 +1211,15 @@ pub(crate) mod test_mocks { use newtype_uuid::{GenericUuid, TypedUuid}; use std::{collections::HashMap, sync::Arc}; use uuid::Uuid; + #[cfg(feature = "sagas")] + use v_model::saga::{ + db::{ModelSagaCachedState, NewSagaEventModel, NewSagaModel, SagaEventModel, SagaModel}, + storage::{ + MockSagaEventStore, MockSagaStore, SagaEventFilter, SagaEventStore, SagaFilter, + SagaStore, + }, + view::{SagaExecNodeId, SagaId}, + }; use v_model::{ AccessGroupId, AccessToken, AccessTokenId, ApiKey, ApiKeyId, ApiUserContactEmail, ApiUserProvider, LinkRequestId, LoginAttemptId, MagicLink, MagicLinkAttempt, @@ -1220,16 +1229,6 @@ pub(crate) mod test_mocks { NewMagicLinkAttempt, NewMagicLinkRedirectUri, NewMagicLinkSecret, NewMapper, OAuthClientId, OAuthRedirectUriId, OAuthSecretId, UserContactEmailId, UserId, UserProviderId, permissions::Caller, - saga::{ - db::{ - ModelSagaCachedState, NewSagaEventModel, NewSagaModel, SagaEventModel, SagaModel, - }, - storage::{ - MockSagaEventStore, MockSagaStore, SagaEventFilter, SagaEventStore, SagaFilter, - SagaStore, - }, - view::{SagaExecNodeId, SagaId}, - }, schema_ext::MagicLinkAttemptState, storage::{ AccessGroupStore, AccessTokenStore, ApiKeyStore, ApiUserContactEmailStore, @@ -1260,15 +1259,14 @@ pub(crate) mod test_mocks { // Construct a mock context that can be used in tests pub async fn mock_context(storage: Arc) -> VContext { let MockKey { signer, verifier } = mock_key("test"); - let mut ctx = VContextBuilder::::new() + let ctx = VContextBuilder::::new() .with_public_url("".to_string()) .with_storage(storage) .with_jwt_expiration(JwtConfig::default().default_expiration) - .with_keys(vec![signer, verifier]) - .with_saga_backend(TypedUuid::new_v4(), None) - .build() - .await - .unwrap(); + .with_keys(vec![signer, verifier]); + #[cfg(feature = "sagas")] + let ctx = ctx.with_saga_backend(TypedUuid::new_v4(), None); + let mut ctx = ctx.build().await.unwrap(); let mapping_engine = Arc::new(DefaultMappingEngine::new( ctx.builtin_registration_user(), @@ -1310,7 +1308,9 @@ pub(crate) mod test_mocks { pub magic_link_secret_store: Option>, pub magic_link_redirect_store: Option>, pub magic_link_attempt_store: Option>, + #[cfg(feature = "sagas")] pub saga_store: Option>, + #[cfg(feature = "sagas")] pub saga_event_store: Option>, } @@ -1333,7 +1333,9 @@ pub(crate) mod test_mocks { magic_link_secret_store: None, magic_link_redirect_store: None, magic_link_attempt_store: None, + #[cfg(feature = "sagas")] saga_store: None, + #[cfg(feature = "sagas")] saga_event_store: None, } } @@ -1955,6 +1957,7 @@ pub(crate) mod test_mocks { } } + #[cfg(feature = "sagas")] #[async_trait] impl SagaStore for MockStorage { async fn get(&self, saga_id: TypedUuid) -> Result, StoreError> { @@ -2021,6 +2024,7 @@ pub(crate) mod test_mocks { } } + #[cfg(feature = "sagas")] #[async_trait] impl SagaEventStore for MockStorage { async fn list(