Files
PiCloud/crates/manager-core/src/repo.rs
MechaCat02 b8b544816d chore: initial scaffold — workspace, docs, blueprint
Sets up the PiCloud monorepo as a Cargo workspace organised around the
three-service architecture (manager / orchestrator / executor), each
backed by a *-core library crate so the same logic powers both the MVP
all-in-one `picloud` binary and the future split-process cluster mode.

  * crates/shared, executor-core, orchestrator-core, manager-core
    define the library surface and trait seams between the three
    services (`ExecutorClient`, `ScriptResolver`, `ScriptRepository`).
  * crates/picloud is the MVP entrypoint; serves /healthz on 8080
    (override via PICLOUD_BIND).
  * crates/picloud-{manager,orchestrator,executor} are skeleton
    binaries that keep the crate boundaries honest until cluster
    mode is built out in v1.3+.
  * docs/git-workflow.md defines the trunk-based workflow:
    short-lived branches, Conventional Commits, separate hotfix
    flow with mandatory reproduction tests.
  * CLAUDE.md captures the working rules for future Claude sessions.

Workspace passes `cargo fmt`, `cargo clippy -D warnings` (with
pedantic enabled), and `cargo test --workspace`. The all-in-one
binary responds on `/healthz` and `/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:16:32 +02:00

84 lines
2.4 KiB
Rust

use async_trait::async_trait;
use picloud_orchestrator_core::{ResolverError, ScriptResolver};
use picloud_shared::{Script, ScriptId};
use sqlx::PgPool;
#[derive(Debug, thiserror::Error)]
pub enum ScriptRepositoryError {
#[error("database error: {0}")]
Db(#[from] sqlx::Error),
#[error("not found: {0}")]
NotFound(ScriptId),
}
/// CRUD over the `scripts` table.
#[async_trait]
pub trait ScriptRepository: Send + Sync {
async fn get(&self, id: ScriptId) -> Result<Option<Script>, ScriptRepositoryError>;
async fn list(&self) -> Result<Vec<Script>, ScriptRepositoryError>;
async fn create(&self, script: &Script) -> Result<(), ScriptRepositoryError>;
async fn update(&self, script: &Script) -> Result<(), ScriptRepositoryError>;
async fn delete(&self, id: ScriptId) -> Result<(), ScriptRepositoryError>;
}
pub struct PostgresScriptRepository {
pool: PgPool,
}
impl PostgresScriptRepository {
#[must_use]
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
// Real query bodies land alongside the first migration. Stubbing the trait
// impl so the workspace compiles and the seam is visible.
#[async_trait]
impl ScriptRepository for PostgresScriptRepository {
async fn get(&self, _id: ScriptId) -> Result<Option<Script>, ScriptRepositoryError> {
let _ = &self.pool;
Ok(None)
}
async fn list(&self) -> Result<Vec<Script>, ScriptRepositoryError> {
Ok(Vec::new())
}
async fn create(&self, _script: &Script) -> Result<(), ScriptRepositoryError> {
Ok(())
}
async fn update(&self, _script: &Script) -> Result<(), ScriptRepositoryError> {
Ok(())
}
async fn delete(&self, _id: ScriptId) -> Result<(), ScriptRepositoryError> {
Ok(())
}
}
/// Adapts a `ScriptRepository` into the `ScriptResolver` trait the
/// orchestrator depends on, so we don't pull the manager into the
/// orchestrator's dependency graph.
pub struct RepoResolver<R: ScriptRepository> {
repo: R,
}
impl<R: ScriptRepository> RepoResolver<R> {
pub fn new(repo: R) -> Self {
Self { repo }
}
}
#[async_trait]
impl<R: ScriptRepository> ScriptResolver for RepoResolver<R> {
async fn resolve(&self, id: ScriptId) -> Result<Option<Script>, ResolverError> {
self.repo
.get(id)
.await
.map_err(|e| ResolverError::Backend(e.to_string()))
}
}