Skip to main content

valence_core/runtime/
factory.rs

1//! Process-global [`ValenceFactory`] from a pinned [`DatabaseRouter`].
2//!
3//! [`RouterValenceFactory::build`] deserializes `actor_json` into a typed [`crate::actor::Actor`]
4//! and attaches it to the returned [`Valence`]. Pass only host-trusted JSON (session-derived);
5//! never deserialize untrusted client payloads as [`Actor::System`](crate::actor::Actor::System).
6//! Install [`crate::actor_policy::RejectExternalSystemActor`] on external paths.
7
8use std::sync::Arc;
9
10use serde_json::Value;
11
12use crate::actor::Actor;
13use crate::actor_policy::{ActorJsonPolicy, ActorTrust};
14use crate::error::{Error, Result};
15use crate::ports::actor::{ActorFactory, JsonActorFactory};
16use crate::ports::endpoints::DatabaseEndpointResolver;
17use crate::ports::secrets::SecretProvider;
18use crate::router::DatabaseRouter;
19use crate::runtime::{Valence, ValenceBuilder};
20use valence_telemetry::TelemetrySink;
21
22/// Factory for reconstructing [`Valence`] instances outside request context.
23pub trait ValenceFactory: Send + Sync + 'static {
24    /// Build a request-scoped [`Valence`] from a JSON actor payload.
25    /// # Errors
26    ///
27    /// Returns an error when the requested operation cannot be completed.
28    fn build(&self, actor_json: &Value) -> Result<Valence>;
29}
30
31/// Host wiring template applied when building from a shared router.
32#[derive(Clone)]
33pub struct RouterValenceFactoryConfig {
34    /// Active backend key passed to [`ValenceBuilder::default_backend_key`].
35    pub default_backend_key: String,
36    /// Optional telemetry sink override (defaults to no-op).
37    pub telemetry_sink: Option<Arc<dyn TelemetrySink>>,
38    /// Optional secret provider override (defaults to no-op).
39    pub secret_provider: Option<Arc<dyn SecretProvider>>,
40    /// Optional actor factory override (defaults to JSON factory).
41    pub actor_factory: Option<Arc<dyn ActorFactory>>,
42    /// Optional endpoint resolver override (defaults to no-op).
43    pub endpoint_resolver: Option<Arc<dyn DatabaseEndpointResolver>>,
44    /// Optional policy for validating `actor_json` (see [`RejectExternalSystemActor`](crate::actor_policy::RejectExternalSystemActor)).
45    pub actor_json_policy: Option<Arc<dyn ActorJsonPolicy>>,
46    /// Trust level passed to [`ActorJsonPolicy`] (default [`ActorTrust::External`]).
47    pub actor_trust: ActorTrust,
48}
49
50impl RouterValenceFactoryConfig {
51    /// Create a config with only the required default backend key.
52    #[must_use]
53    pub fn new(default_backend_key: impl Into<String>) -> Self {
54        Self {
55            default_backend_key: default_backend_key.into(),
56            telemetry_sink: None,
57            secret_provider: None,
58            actor_factory: None,
59            endpoint_resolver: None,
60            actor_json_policy: None,
61            actor_trust: ActorTrust::External,
62        }
63    }
64
65    /// Install an [`ActorJsonPolicy`] for factory `actor_json` validation.
66    #[must_use]
67    pub fn actor_json_policy(mut self, policy: impl ActorJsonPolicy + 'static) -> Self {
68        self.actor_json_policy = Some(Arc::new(policy));
69        self
70    }
71}
72
73/// [`ValenceFactory`] backed by a shared [`DatabaseRouter`].
74#[derive(Clone)]
75pub struct RouterValenceFactory {
76    router: Arc<DatabaseRouter>,
77    config: RouterValenceFactoryConfig,
78}
79
80impl RouterValenceFactory {
81    /// Wrap a shared router and host wiring template.
82    #[must_use]
83    pub fn new(router: Arc<DatabaseRouter>, config: RouterValenceFactoryConfig) -> Self {
84        Self { router, config }
85    }
86
87    /// Return an [`Arc`] factory suitable for dependency injection.
88    pub fn arc(
89        router: Arc<DatabaseRouter>,
90        config: RouterValenceFactoryConfig,
91    ) -> Arc<dyn ValenceFactory> {
92        Arc::new(Self::new(router, config))
93    }
94}
95
96impl ValenceFactory for RouterValenceFactory {
97    fn build(&self, actor_json: &Value) -> Result<Valence> {
98        if let Some(policy) = &self.config.actor_json_policy {
99            policy.validate(self.config.actor_trust, actor_json)?;
100        }
101
102        let actor_factory = self
103            .config
104            .actor_factory
105            .clone()
106            .unwrap_or_else(|| Arc::new(JsonActorFactory));
107        let _actor_ctx = actor_factory.build(actor_json)?;
108
109        // Bind the typed actor into the runtime. Hosts must only pass trusted JSON
110        // (e.g. session-derived); never deserialize untrusted client payloads as `Actor::System`.
111        let actor: Actor = serde_json::from_value(actor_json.clone()).map_err(|e| {
112            Error::Validation(format!("actor_json is not a valid valence::Actor: {e}"))
113        })?;
114
115        let mut builder = ValenceBuilder::new()
116            .database_router(Arc::clone(&self.router))
117            .default_backend_key(self.config.default_backend_key.clone())
118            .actor_factory(actor_factory)
119            .with_actor(actor);
120
121        if let Some(sink) = &self.config.telemetry_sink {
122            builder = builder.telemetry_sink(Arc::clone(sink));
123        }
124        if let Some(secrets) = &self.config.secret_provider {
125            builder = builder.secret_provider(Arc::clone(secrets));
126        }
127        if let Some(endpoints) = &self.config.endpoint_resolver {
128            builder = builder.endpoint_resolver(Arc::clone(endpoints));
129        }
130
131        builder.build()
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::actor::Actor;
139    use crate::backend::DatabaseBackend;
140    use async_trait::async_trait;
141    use std::sync::Arc;
142
143    #[derive(Debug)]
144    struct MockBackend;
145
146    #[async_trait]
147    impl DatabaseBackend for MockBackend {
148        fn engine_id(&self) -> &'static str {
149            "mem"
150        }
151
152        fn capabilities(&self) -> crate::backend::BackendCapabilities {
153            crate::backend::BackendCapabilities::mem()
154        }
155
156        async fn execute_compiled_query(
157            &self,
158            _compiled: &crate::compiled_query::CompiledQuery,
159        ) -> crate::error::Result<Vec<serde_json::Value>> {
160            Ok(vec![])
161        }
162
163        async fn get_record(
164            &self,
165            _table: &str,
166            _id: &str,
167        ) -> crate::error::Result<Option<serde_json::Value>> {
168            Ok(None)
169        }
170
171        async fn create_record(
172            &self,
173            _table: &str,
174            _content: serde_json::Value,
175        ) -> crate::error::Result<serde_json::Value> {
176            Ok(serde_json::json!({}))
177        }
178
179        async fn update_record(
180            &self,
181            _table: &str,
182            _id: &str,
183            _content: serde_json::Value,
184        ) -> crate::error::Result<serde_json::Value> {
185            Ok(serde_json::json!({}))
186        }
187
188        async fn upsert_record(
189            &self,
190            _table: &str,
191            _id: &str,
192            _content: serde_json::Value,
193        ) -> crate::error::Result<serde_json::Value> {
194            Ok(serde_json::json!({}))
195        }
196
197        async fn delete_record(&self, _table: &str, _id: &str) -> crate::error::Result<()> {
198            Ok(())
199        }
200
201        async fn relate_edge(
202            &self,
203            _from: &crate::record_id::RecordId,
204            _edge_table: &str,
205            _to: &crate::record_id::RecordId,
206        ) -> crate::error::Result<()> {
207            Ok(())
208        }
209
210        async fn unrelate_edge(
211            &self,
212            _from: &crate::record_id::RecordId,
213            _edge_table: &str,
214            _to: &crate::record_id::RecordId,
215        ) -> crate::error::Result<()> {
216            Ok(())
217        }
218
219        async fn get_edge_targets(
220            &self,
221            _from: &crate::record_id::RecordId,
222            _edge_table: &str,
223        ) -> crate::error::Result<Vec<crate::record_id::RecordId>> {
224            Ok(vec![])
225        }
226    }
227
228    fn factory() -> RouterValenceFactory {
229        let valence = Valence::builder()
230            .add_backend("default", Arc::new(MockBackend))
231            .build()
232            .expect("build");
233        let router = Arc::clone(valence.database_router());
234        RouterValenceFactory::new(router, RouterValenceFactoryConfig::new("default"))
235    }
236
237    #[test]
238    fn build_binds_user_actor_from_json() {
239        let f = factory();
240        let actor_json = serde_json::to_value(Actor::User {
241            user_id: "u1".into(),
242        })
243        .expect("json");
244        let v = f.build(&actor_json).expect("build");
245        assert_eq!(v.actor().user_id(), Some("u1"));
246    }
247
248    #[test]
249    fn build_rejects_invalid_actor_json() {
250        let f = factory();
251        let err = f
252            .build(&serde_json::json!({"kind": "not-an-actor"}))
253            .expect_err("invalid");
254        assert!(err.to_string().contains("actor_json"));
255    }
256
257    #[test]
258    fn build_rejects_external_system_when_policy_installed() {
259        use crate::actor_policy::RejectExternalSystemActor;
260        let valence = Valence::builder()
261            .add_backend("default", Arc::new(MockBackend))
262            .build()
263            .expect("build");
264        let router = Arc::clone(valence.database_router());
265        let config =
266            RouterValenceFactoryConfig::new("default").actor_json_policy(RejectExternalSystemActor);
267        let f = RouterValenceFactory::new(router, config);
268        let system = serde_json::to_value(Actor::System {
269            operation: "x".into(),
270        })
271        .expect("json");
272        let err = f.build(&system).expect_err("system");
273        assert!(err.to_string().contains("System"));
274    }
275}