Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **GeoIP feature split for offline-safe builds.** The `geoip` cargo feature now pulls only the pure-Rust runtime MaxMind MMDB reader (`maxminddb`) and is included in the default `full` preset — no build-time network fetch. The bundled DB-IP Country Lite database (and its build-time download) moved to a new opt-in `geoip-embedded` feature. **Breaking:** zero-config `country` enrichment now requires building with `--features geoip-embedded`; the default build enriches `country`/`city` only when `signals.geoip_db_path` points at a GeoLite2-City MMDB. This makes the default preset build cleanly in air-gapped/offline environments.

### Internal

- Removed duplication across the codebase: shared attribute parsing in `forge-macros`, declarative macros for the sealed handler-context impls and the eight test-context builders in `forge-core`, a shared store core in `@forge-rs/svelte`, and a smaller `Forge::run()` that no longer mutates its config in place. No public API change.

## [0.10.2] - 2026-05-22

### Changed
Expand Down
156 changes: 52 additions & 104 deletions crates/forge-core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,33 @@ pub trait AuthenticatedContext: HandlerContext {
fn tenant_id(&self) -> Option<Uuid>;
}

impl HandlerContext for crate::function::QueryContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
/// Forward [`HandlerContext`] to each context type's inherent `db()`/`db_conn()`.
macro_rules! impl_handler_context {
($($ty:ty),+ $(,)?) => {
$(
impl HandlerContext for $ty {
fn db(&self) -> ForgeDb { self.db() }
fn db_conn(&self) -> DbConn<'_> { self.db_conn() }
}
)+
};
}

impl_handler_context!(
crate::function::QueryContext,
crate::job::JobContext,
crate::cron::CronContext,
crate::daemon::DaemonContext,
crate::webhook::WebhookContext,
crate::workflow::WorkflowContext,
crate::mcp::McpToolContext,
);

// MutationContext is the one exception: its inherent `db()` returns a
// transaction-bound handle, so the trait impl exposes the pool-backed view
// that intentionally bypasses the active transaction.
impl HandlerContext for crate::function::MutationContext {
fn db(&self) -> ForgeDb {
// MutationContext::tx() returns DbConn, not ForgeDb.
// For HandlerContext we expose the pool-backed ForgeDb view, which
// intentionally bypasses the active transaction.
crate::function::ForgeDb::from_pool(self.pool_outside_transaction())
}

Expand All @@ -95,101 +107,37 @@ impl HandlerContext for crate::function::MutationContext {
}
}

impl HandlerContext for crate::job::JobContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
}

impl HandlerContext for crate::cron::CronContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
}

impl HandlerContext for crate::daemon::DaemonContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
}

impl HandlerContext for crate::webhook::WebhookContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
}

impl HandlerContext for crate::workflow::WorkflowContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
}

impl HandlerContext for crate::mcp::McpToolContext {
fn db(&self) -> ForgeDb {
self.db()
}

fn db_conn(&self) -> DbConn<'_> {
self.db_conn()
}
}

impl AuthenticatedContext for crate::function::QueryContext {
fn user_id(&self) -> crate::error::Result<Uuid> {
self.user_id()
}

fn tenant_id(&self) -> Option<Uuid> {
self.tenant_id()
}
}

impl AuthenticatedContext for crate::function::MutationContext {
fn user_id(&self) -> crate::error::Result<Uuid> {
self.user_id()
}

fn tenant_id(&self) -> Option<Uuid> {
self.tenant_id()
}
/// Forward [`AuthenticatedContext`] to each context type's inherent accessors.
macro_rules! impl_authenticated_context {
($($ty:ty),+ $(,)?) => {
$(
impl AuthenticatedContext for $ty {
fn user_id(&self) -> crate::error::Result<Uuid> { self.user_id() }
fn tenant_id(&self) -> Option<Uuid> { self.tenant_id() }
}
)+
};
}

impl AuthenticatedContext for crate::mcp::McpToolContext {
fn user_id(&self) -> crate::error::Result<Uuid> {
self.user_id()
}
impl_authenticated_context!(
crate::function::QueryContext,
crate::function::MutationContext,
crate::mcp::McpToolContext,
);

fn tenant_id(&self) -> Option<Uuid> {
self.tenant_id()
}
macro_rules! impl_sealed {
($($ty:ty),+ $(,)?) => {
$( impl Sealed for $ty {} )+
};
}

impl Sealed for crate::function::QueryContext {}
impl Sealed for crate::function::MutationContext {}
impl Sealed for crate::job::JobContext {}
impl Sealed for crate::cron::CronContext {}
impl Sealed for crate::daemon::DaemonContext {}
impl Sealed for crate::webhook::WebhookContext {}
impl Sealed for crate::workflow::WorkflowContext {}
impl Sealed for crate::mcp::McpToolContext {}
impl_sealed!(
crate::function::QueryContext,
crate::function::MutationContext,
crate::job::JobContext,
crate::cron::CronContext,
crate::daemon::DaemonContext,
crate::webhook::WebhookContext,
crate::workflow::WorkflowContext,
crate::mcp::McpToolContext,
);
47 changes: 3 additions & 44 deletions crates/forge-core/src/testing/context/cron.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ pub struct TestCronContextBuilder {
env_vars: HashMap<String, String>,
}

impl_test_auth_builder!(TestCronContextBuilder);
impl_test_env_builder!(TestCronContextBuilder);

impl TestCronContextBuilder {
pub fn new(cron_name: impl Into<String>) -> Self {
let now = Utc::now();
Expand Down Expand Up @@ -189,39 +192,6 @@ impl TestCronContextBuilder {
self.is_catch_up = true;
self
}

pub fn as_user(mut self, id: Uuid) -> Self {
self.user_id = Some(id);
self
}

/// For non-UUID auth providers (Firebase, Clerk, etc.).
pub fn as_subject(mut self, subject: impl Into<String>) -> Self {
self.claims
.insert("sub".to_string(), serde_json::json!(subject.into()));
self
}

pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}

pub fn with_roles(mut self, roles: Vec<String>) -> Self {
self.roles.extend(roles);
self
}

pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.claims.insert(key.into(), value);
self
}

pub fn with_pool(mut self, pool: PgPool) -> Self {
self.pool = Some(pool);
self
}

pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
where
F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
Expand All @@ -234,17 +204,6 @@ impl TestCronContextBuilder {
let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
}

pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}

pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
self.env_vars.extend(vars);
self
}

pub fn build(self) -> TestCronContext {
TestCronContext {
run_id: self.run_id.unwrap_or_else(Uuid::new_v4),
Expand Down
19 changes: 2 additions & 17 deletions crates/forge-core/src/testing/context/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub struct TestDaemonContextBuilder {
env_vars: HashMap<String, String>,
}

impl_test_env_builder!(TestDaemonContextBuilder);

impl TestDaemonContextBuilder {
pub fn new(daemon_name: impl Into<String>) -> Self {
Self {
Expand All @@ -97,12 +99,6 @@ impl TestDaemonContextBuilder {
self.instance_id = Some(id);
self
}

pub fn with_pool(mut self, pool: PgPool) -> Self {
self.pool = Some(pool);
self
}

pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
where
F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
Expand All @@ -115,17 +111,6 @@ impl TestDaemonContextBuilder {
let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
}

pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}

pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
self.env_vars.extend(vars);
self
}

pub fn build(self) -> TestDaemonContext {
let (shutdown_tx, shutdown_rx) = watch::channel(false);

Expand Down
54 changes: 3 additions & 51 deletions crates/forge-core/src/testing/context/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ pub struct TestJobContextBuilder {
cancel_requested: bool,
}

impl_test_auth_builder!(TestJobContextBuilder);
impl_test_env_builder!(TestJobContextBuilder);

impl TestJobContextBuilder {
/// Create a new builder with job type.
pub fn new(job_type: impl Into<String>) -> Self {
Expand Down Expand Up @@ -239,44 +242,6 @@ impl TestJobContextBuilder {
self.max_attempts = 3;
self
}

/// Set the authenticated user with a UUID.
pub fn as_user(mut self, id: Uuid) -> Self {
self.user_id = Some(id);
self
}

/// For non-UUID auth providers (Firebase, Clerk, etc.).
pub fn as_subject(mut self, subject: impl Into<String>) -> Self {
self.claims
.insert("sub".to_string(), serde_json::json!(subject.into()));
self
}

/// Add a role.
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}

/// Add multiple roles.
pub fn with_roles(mut self, roles: Vec<String>) -> Self {
self.roles.extend(roles);
self
}

/// Add a custom claim.
pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.claims.insert(key.into(), value);
self
}

/// Set the database pool.
pub fn with_pool(mut self, pool: PgPool) -> Self {
self.pool = Some(pool);
self
}

/// Add an HTTP mock with a custom handler.
pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
where
Expand All @@ -291,19 +256,6 @@ impl TestJobContextBuilder {
let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
}

/// Set a single environment variable.
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}

/// Set multiple environment variables.
pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
self.env_vars.extend(vars);
self
}

/// Start with cancellation already requested.
///
/// Use this to test how jobs handle cancellation from the start.
Expand Down
Loading
Loading