greentic_runner_host/runtime_refs.rs
1//! Per-pack `runtime://` URI resolution for the C5 runtime-refs channel.
2//!
3//! `pack-config.v1.runtime_refs` binds opaque keys to `runtime://<env>/...`
4//! URIs whose values live in the env-pack-emitted `runtime.json`
5//! ([`EnvironmentRuntime`]). The producer (greentic-start) owns the
6//! `EnvironmentRuntime` snapshot + hot-reload watcher and exposes lookups
7//! through [`RuntimeRefResolver`].
8//!
9//! Runner-host stays unaware of URI shapes: the per-pack `refs` map carries
10//! opaque URI strings, and the resolver opaquely returns the current value.
11//! This shape preserves hot-reload — every host-import call re-enters the
12//! resolver, which reads from start's `ArcSwap` snapshot.
13//!
14//! [`EnvironmentRuntime`]: greentic-deploy-spec::EnvironmentRuntime
15
16use serde_json::Value;
17use std::collections::BTreeMap;
18use std::fmt::Debug;
19use std::sync::Arc;
20use thiserror::Error;
21
22/// Errors a [`RuntimeRefResolver`] can return. Mapped onto
23/// `greentic:runtime-config@1.0.0::ConfigError` by the host import. The
24/// "not bound in the current snapshot" case is signalled by `Ok(None)`
25/// from [`RuntimeRefResolver::resolve`], not by an error.
26#[derive(Debug, Error)]
27pub enum RuntimeRefResolverError {
28 /// URI shape was rejected by the resolver (parse / env mismatch).
29 #[error("runtime-ref invalid: {0}")]
30 Invalid(String),
31 /// Resolver was unable to complete the lookup (snapshot read failure,
32 /// store error, etc.). Treated by the host import as `Internal`.
33 #[error("runtime-ref internal error: {0}")]
34 Internal(String),
35}
36
37/// Trait implemented by greentic-start to resolve `runtime://` URIs against
38/// the live [`EnvironmentRuntime`] snapshot. Implementations MUST be cheap
39/// to call (they are invoked on every `greentic:runtime-config/get`).
40///
41/// [`EnvironmentRuntime`]: greentic-deploy-spec::EnvironmentRuntime
42pub trait RuntimeRefResolver: Debug + Send + Sync {
43 /// Resolve a single `runtime://` URI. Returns `Ok(None)` when the URI
44 /// parses cleanly but is not bound in the current snapshot — the host
45 /// import maps this to `ConfigError::NotFound`.
46 fn resolve(&self, runtime_ref: &str) -> Result<Option<Value>, RuntimeRefResolverError>;
47}
48
49/// Per-pack runtime-refs injection: the `key → URI` map from
50/// `pack-config.v1.runtime_refs` plus the env-wide resolver. The resolver
51/// is shared across every pack in the env (one `ArcSwap` snapshot owner);
52/// the `refs` map is per-pack.
53#[derive(Clone, Debug)]
54pub struct RuntimeRefsInjection {
55 /// `key → "runtime://<env>/discovered/<path>"`. Keys are what the WASM
56 /// component asks for; URIs are opaque to runner-host and forwarded to
57 /// the resolver verbatim.
58 pub refs: Arc<BTreeMap<String, String>>,
59 /// Env-shared resolver. Reads start's `EnvironmentRuntime` snapshot on
60 /// every call so hot-reloads land immediately.
61 pub resolver: Arc<dyn RuntimeRefResolver>,
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 /// Test resolver that returns a fixed value for one URI.
69 #[derive(Debug)]
70 struct FixedResolver {
71 uri: String,
72 value: Value,
73 }
74
75 impl RuntimeRefResolver for FixedResolver {
76 fn resolve(&self, runtime_ref: &str) -> Result<Option<Value>, RuntimeRefResolverError> {
77 if runtime_ref == self.uri {
78 Ok(Some(self.value.clone()))
79 } else {
80 Ok(None)
81 }
82 }
83 }
84
85 #[test]
86 fn resolver_returns_value_for_bound_uri() {
87 let resolver = FixedResolver {
88 uri: "runtime://local/discovered/alb_dns".into(),
89 value: Value::String("alb.example.com".into()),
90 };
91 let value = resolver
92 .resolve("runtime://local/discovered/alb_dns")
93 .unwrap();
94 assert_eq!(value, Some(Value::String("alb.example.com".into())));
95 }
96
97 #[test]
98 fn resolver_returns_none_for_unbound_uri() {
99 let resolver = FixedResolver {
100 uri: "runtime://local/discovered/alb_dns".into(),
101 value: Value::String("alb.example.com".into()),
102 };
103 assert_eq!(
104 resolver
105 .resolve("runtime://local/discovered/other")
106 .unwrap(),
107 None,
108 );
109 }
110}