//! `ModuleSource` — the v1.1.3 Rhai module-loading contract. //! //! The executor-core `PicloudModuleResolver` calls into this trait to //! load `kind = 'module'` scripts referenced by `import "" as ;` //! statements. The Postgres impl in `manager-core` reads from the //! `scripts` table; tests pin in-memory fakes. //! //! Implementations MUST derive `app_id` from `cx.app_id` and pass it //! to every backend query. The `name` argument carries only the //! script's name (the literal between the import quotes); the trait //! has no way to express a cross-app lookup. That asymmetry is the //! load-bearing cross-app isolation boundary — see `docs/sdk-shape.md`. use async_trait::async_trait; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{AppId, ScriptId, SdkCallCx}; /// A module script as returned by `ModuleSource::lookup`. Carries only /// the fields the resolver needs: the id (for diagnostics), the source /// (to compile), and `updated_at` (the cache-staleness comparator). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModuleScript { pub script_id: ScriptId, pub app_id: AppId, pub name: String, pub source: String, pub updated_at: DateTime, } /// Lookup contract used by `PicloudModuleResolver`. `lookup` MUST /// scope by `cx.app_id`; cross-app reads must be unreachable. #[async_trait] pub trait ModuleSource: Send + Sync { /// Resolve a module script by `(cx.app_id, name)`. Returns `None` /// when no row exists, or when a row exists but its `kind` is /// `'endpoint'` (endpoints are never importable). The resolver /// surfaces `None` as `ErrorModuleNotFound` to Rhai. async fn lookup( &self, cx: &SdkCallCx, name: &str, ) -> Result, ModuleSourceError>; } /// Failure modes surfaced from `ModuleSource::lookup`. "Not found" is /// not exceptional — it's `Ok(None)`. #[derive(Debug, Error)] pub enum ModuleSourceError { /// Backend (Postgres, network, etc.) unavailable or returned an /// error. The string is safe to surface to a script (Rhai wraps /// it in `ErrorModuleNotFound` with the module name + reason). #[error("module backend error: {0}")] Backend(String), } /// Stub used by the executor-core test harness so engine integration /// tests don't need a real DB-backed source. Every lookup returns /// `Ok(None)` — `import "x"` always errors as "module not found" /// under this impl. #[derive(Debug, Default, Clone, Copy)] pub struct NoopModuleSource; #[async_trait] impl ModuleSource for NoopModuleSource { async fn lookup( &self, _cx: &SdkCallCx, _name: &str, ) -> Result, ModuleSourceError> { Ok(None) } }