fix(manager-core): F-Q-011 blanket Arc<T: ScriptRepository> impl; delete PostgresScriptRepoHandle
A 70-line newtype lived in picloud/src/lib.rs wrapping Arc<PostgresScriptRepository> and re-implementing every ScriptRepository method as `self.0.method(...).await`. Hand-delegation invited silent skew when the trait gained a method, and forced 8 call sites to know about the wrapper. Add a blanket impl<T: ScriptRepository + ?Sized> ScriptRepository for Arc<T> in manager-core::repo. The implementations forward via (**self).method(...) so any owner of an Arc<dyn ScriptRepository> (or Arc<ConcreteImpl>) can be passed where the trait is expected. Migrate the 7 picloud-binary call sites: Arc::new(PostgresScriptRepoHandle(script_repo.clone())) → script_repo.clone() Delete the newtype + its hand-delegated impl. Leaves a 6-line comment documenting why the boilerplate is gone. AUDIT.md anchor: F-Q-011. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,68 @@ pub trait ScriptRepository: Send + Sync {
|
||||
-> Result<Vec<Script>, ScriptRepositoryError>;
|
||||
}
|
||||
|
||||
/// F-Q-011: blanket impl for `Arc<T: ScriptRepository>`. Lets callers
|
||||
/// pass a shared `Arc<PostgresScriptRepository>` directly anywhere a
|
||||
/// `dyn ScriptRepository` is wanted — replacing the hand-delegated
|
||||
/// `PostgresScriptRepoHandle` newtype that previously had to be
|
||||
/// rewritten in lockstep every time the trait gained a method.
|
||||
#[async_trait::async_trait]
|
||||
impl<T: ScriptRepository + ?Sized> ScriptRepository for std::sync::Arc<T> {
|
||||
async fn get(&self, id: ScriptId) -> Result<Option<Script>, ScriptRepositoryError> {
|
||||
(**self).get(id).await
|
||||
}
|
||||
async fn get_by_name(
|
||||
&self,
|
||||
app_id: AppId,
|
||||
name: &str,
|
||||
) -> Result<Option<Script>, ScriptRepositoryError> {
|
||||
(**self).get_by_name(app_id, name).await
|
||||
}
|
||||
async fn list(&self) -> Result<Vec<Script>, ScriptRepositoryError> {
|
||||
(**self).list().await
|
||||
}
|
||||
async fn list_for_app(&self, app_id: AppId) -> Result<Vec<Script>, ScriptRepositoryError> {
|
||||
(**self).list_for_app(app_id).await
|
||||
}
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
user_id: AdminUserId,
|
||||
) -> Result<Vec<Script>, ScriptRepositoryError> {
|
||||
(**self).list_for_user(user_id).await
|
||||
}
|
||||
async fn create(&self, input: NewScript) -> Result<Script, ScriptRepositoryError> {
|
||||
(**self).create(input).await
|
||||
}
|
||||
async fn update(
|
||||
&self,
|
||||
id: ScriptId,
|
||||
patch: ScriptPatch,
|
||||
) -> Result<Script, ScriptRepositoryError> {
|
||||
(**self).update(id, patch).await
|
||||
}
|
||||
async fn delete(&self, id: ScriptId) -> Result<(), ScriptRepositoryError> {
|
||||
(**self).delete(id).await
|
||||
}
|
||||
async fn count_routes_for_script(
|
||||
&self,
|
||||
script_id: ScriptId,
|
||||
) -> Result<i64, ScriptRepositoryError> {
|
||||
(**self).count_routes_for_script(script_id).await
|
||||
}
|
||||
async fn count_triggers_for_script(
|
||||
&self,
|
||||
script_id: ScriptId,
|
||||
) -> Result<i64, ScriptRepositoryError> {
|
||||
(**self).count_triggers_for_script(script_id).await
|
||||
}
|
||||
async fn list_imports(
|
||||
&self,
|
||||
script_id: ScriptId,
|
||||
) -> Result<Vec<Script>, ScriptRepositoryError> {
|
||||
(**self).list_imports(script_id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Inbound shape for create. Defaults match the migration's CHECK
|
||||
/// constraints; the repo enforces them in the DB regardless.
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -299,7 +299,7 @@ pub async fn build_app(
|
||||
// that the dispatcher fires through the standard executor path.
|
||||
let invoke: Arc<dyn picloud_shared::InvokeService> = Arc::new(
|
||||
picloud_manager_core::invoke_service::InvokeServiceImpl::new(
|
||||
Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
||||
script_repo.clone(),
|
||||
route_table.clone(),
|
||||
outbox_repo.clone(),
|
||||
),
|
||||
@@ -348,9 +348,7 @@ pub async fn build_app(
|
||||
.collect();
|
||||
app_domain_table.replace(compiled_domains);
|
||||
|
||||
let resolver = Arc::new(RepoResolver::new(PostgresScriptRepoHandle(
|
||||
script_repo.clone(),
|
||||
)));
|
||||
let resolver = Arc::new(RepoResolver::new(script_repo.clone()));
|
||||
// Single global gate — overflow is rejected with 503 + Retry-After.
|
||||
// See `ExecutionGate` docs and `PICLOUD_MAX_CONCURRENT_EXECUTIONS`.
|
||||
let gate = Arc::new(ExecutionGate::from_env());
|
||||
@@ -360,7 +358,7 @@ pub async fn build_app(
|
||||
// due rows to the executor. Shares the `ExecutionGate` with sync
|
||||
// HTTP per design notes §2 (one cap for everything).
|
||||
let dispatcher_script_repo: Arc<dyn ScriptRepository> =
|
||||
Arc::new(PostgresScriptRepoHandle(script_repo.clone()));
|
||||
script_repo.clone();
|
||||
let principals: Arc<dyn PrincipalResolver> =
|
||||
Arc::new(AdminPrincipalResolver::new(auth.users.clone()));
|
||||
// The InboxRegistry is constructed once and shared between the
|
||||
@@ -385,7 +383,7 @@ pub async fn build_app(
|
||||
.spawn();
|
||||
|
||||
let admin = AdminState {
|
||||
repo: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
||||
repo: script_repo.clone(),
|
||||
logs: log_repo,
|
||||
apps: apps_repo.clone(),
|
||||
authz: authz.clone(),
|
||||
@@ -394,7 +392,7 @@ pub async fn build_app(
|
||||
};
|
||||
let route_admin = RouteAdminState {
|
||||
routes: route_repo.clone(),
|
||||
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
||||
scripts: script_repo.clone(),
|
||||
domains: domains_repo.clone(),
|
||||
table: route_table.clone(),
|
||||
authz: authz.clone(),
|
||||
@@ -439,7 +437,7 @@ pub async fn build_app(
|
||||
triggers: trigger_repo.clone(),
|
||||
apps: apps_repo.clone(),
|
||||
authz: authz.clone(),
|
||||
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
||||
scripts: script_repo.clone(),
|
||||
config: trigger_config,
|
||||
master_key: master_key.clone(),
|
||||
};
|
||||
@@ -533,7 +531,7 @@ pub async fn build_app(
|
||||
triggers: trigger_repo_for_queues.clone(),
|
||||
apps: apps_repo.clone(),
|
||||
authz: authz.clone(),
|
||||
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
||||
scripts: script_repo.clone(),
|
||||
},
|
||||
))
|
||||
.merge(files_admin_router(files_admin_state))
|
||||
@@ -638,82 +636,10 @@ async fn version() -> Json<serde_json::Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Bridge: a single `PostgresScriptRepository` Arc is shared between the
|
||||
// admin router (writes) and the resolver (reads). The resolver wants
|
||||
// owned `impl ScriptRepository`, so we wrap the Arc in a delegating
|
||||
// handle here rather than instantiating two repos against the same pool.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct PostgresScriptRepoHandle(Arc<PostgresScriptRepository>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl picloud_manager_core::ScriptRepository for PostgresScriptRepoHandle {
|
||||
async fn get(
|
||||
&self,
|
||||
id: picloud_shared::ScriptId,
|
||||
) -> Result<Option<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.get(id).await
|
||||
}
|
||||
async fn get_by_name(
|
||||
&self,
|
||||
app_id: picloud_shared::AppId,
|
||||
name: &str,
|
||||
) -> Result<Option<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.get_by_name(app_id, name).await
|
||||
}
|
||||
async fn list(
|
||||
&self,
|
||||
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.list().await
|
||||
}
|
||||
async fn list_for_app(
|
||||
&self,
|
||||
app_id: picloud_shared::AppId,
|
||||
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.list_for_app(app_id).await
|
||||
}
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
user_id: picloud_shared::AdminUserId,
|
||||
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.list_for_user(user_id).await
|
||||
}
|
||||
async fn create(
|
||||
&self,
|
||||
input: picloud_manager_core::NewScript,
|
||||
) -> Result<picloud_shared::Script, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.create(input).await
|
||||
}
|
||||
async fn update(
|
||||
&self,
|
||||
id: picloud_shared::ScriptId,
|
||||
patch: picloud_manager_core::ScriptPatch,
|
||||
) -> Result<picloud_shared::Script, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.update(id, patch).await
|
||||
}
|
||||
async fn delete(
|
||||
&self,
|
||||
id: picloud_shared::ScriptId,
|
||||
) -> Result<(), picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.delete(id).await
|
||||
}
|
||||
async fn count_routes_for_script(
|
||||
&self,
|
||||
script_id: picloud_shared::ScriptId,
|
||||
) -> Result<i64, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.count_routes_for_script(script_id).await
|
||||
}
|
||||
async fn count_triggers_for_script(
|
||||
&self,
|
||||
script_id: picloud_shared::ScriptId,
|
||||
) -> Result<i64, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.count_triggers_for_script(script_id).await
|
||||
}
|
||||
async fn list_imports(
|
||||
&self,
|
||||
script_id: picloud_shared::ScriptId,
|
||||
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
|
||||
self.0.list_imports(script_id).await
|
||||
}
|
||||
}
|
||||
// F-Q-011: the PostgresScriptRepoHandle newtype lived here so the
|
||||
// resolver could take an owned `impl ScriptRepository` from the shared
|
||||
// Arc<PostgresScriptRepository>. The blanket
|
||||
// `impl<T: ScriptRepository + ?Sized> ScriptRepository for Arc<T>` in
|
||||
// manager-core::repo lets us pass `script_repo.clone()` directly,
|
||||
// eliminating 70 lines of hand-delegated boilerplate that had to be
|
||||
// rewritten every time the trait gained a method.
|
||||
|
||||
Reference in New Issue
Block a user