greentic_runner_host/
provider.rs

1use std::str::FromStr;
2use std::sync::Arc;
3
4use anyhow::{Context, Result, anyhow, bail};
5use greentic_types::{EnvId, PackManifest, StateKey as StoreStateKey, TenantCtx, TenantId};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::storage::DynStateStore;
10use crate::storage::state::STATE_PREFIX;
11
12#[derive(Clone, Debug, Serialize)]
13pub struct ProviderBinding {
14    pub provider_id: Option<String>,
15    pub provider_type: String,
16    pub component_ref: String,
17    pub export: String,
18    pub world: String,
19    pub config_json: Option<String>,
20    pub pack_ref: Option<String>,
21}
22
23#[derive(Clone, Debug, Deserialize)]
24struct ProviderInstance {
25    provider_id: String,
26    provider_type: String,
27    pack_ref: Option<String>,
28    component_ref: String,
29    export: String,
30    world: String,
31    #[serde(default)]
32    enabled: bool,
33    #[serde(default)]
34    config: Value,
35}
36
37#[derive(Clone, Debug, Deserialize)]
38struct ProviderExtRuntime {
39    component_ref: String,
40    export: String,
41    world: String,
42}
43
44#[derive(Clone, Debug, Deserialize)]
45#[allow(dead_code)]
46struct ProviderExtDecl {
47    #[serde(default)]
48    provider_id: Option<String>,
49    provider_type: String,
50    #[serde(default)]
51    capabilities: Vec<String>,
52    #[serde(default)]
53    ops: Vec<String>,
54    #[serde(default)]
55    config_schema_ref: Option<String>,
56    #[serde(default)]
57    state_schema_ref: Option<String>,
58    runtime: ProviderExtRuntime,
59    #[serde(default)]
60    docs_ref: Option<String>,
61}
62
63#[derive(Clone)]
64pub struct ProviderRegistry {
65    pack_ref: Option<String>,
66    inline: Vec<ProviderExtDecl>,
67    state_store: Option<DynStateStore>,
68    tenant: TenantCtx,
69}
70
71impl ProviderRegistry {
72    pub fn new(
73        manifest: &PackManifest,
74        state_store: Option<DynStateStore>,
75        tenant: &str,
76        env: &str,
77    ) -> Result<Self> {
78        let inline = extract_inline_providers(manifest)?;
79        let tenant_ctx = TenantCtx::new(
80            EnvId::from_str(env).unwrap_or_else(|_| EnvId::from_str("local").expect("local env")),
81            TenantId::from_str(tenant).with_context(|| format!("invalid tenant id `{tenant}`"))?,
82        );
83        let pack_ref = Some(format!(
84            "{}@{}",
85            manifest.pack_id.as_str(),
86            manifest.version
87        ));
88        Ok(Self {
89            pack_ref,
90            inline,
91            state_store,
92            tenant: tenant_ctx,
93        })
94    }
95
96    pub fn resolve(
97        &self,
98        provider_id: Option<&str>,
99        provider_type: Option<&str>,
100    ) -> Result<ProviderBinding> {
101        if provider_id.is_none() && provider_type.is_none() {
102            bail!("provider.invoke requires provider_id or provider_type");
103        }
104
105        if let Some(id) = provider_id {
106            if let Some(binding) = self.load_instance(id)? {
107                return Ok(binding);
108            }
109            if let Some(ext) = self
110                .inline
111                .iter()
112                .find(|decl| decl.provider_id.as_deref() == Some(id))
113            {
114                return Ok(binding_from_decl(ext, self.pack_ref.clone(), None));
115            }
116            bail!("provider_id `{id}` not found");
117        }
118
119        let provider_type = provider_type.unwrap();
120        let matches: Vec<_> = self
121            .inline
122            .iter()
123            .filter(|decl| decl.provider_type == provider_type)
124            .collect();
125        match matches.as_slice() {
126            [] => bail!("no provider runtime found for type `{provider_type}`"),
127            [decl] => Ok(binding_from_decl(
128                decl,
129                self.pack_ref.clone(),
130                Some(provider_type.to_string()),
131            )),
132            _ => bail!("multiple providers found for type `{provider_type}`, specify provider_id"),
133        }
134    }
135
136    fn load_instance(&self, provider_id: &str) -> Result<Option<ProviderBinding>> {
137        let store = match &self.state_store {
138            Some(store) => Arc::clone(store),
139            None => return Ok(None),
140        };
141        let key = StoreStateKey::from(format!("providers/instances/{provider_id}.json"));
142        let value = store
143            .get_json(&self.tenant, STATE_PREFIX, &key, None)
144            .map_err(|err| anyhow!(err.to_string()))
145            .with_context(|| format!("failed to load provider instance `{provider_id}`"))?;
146        let Some(doc) = value else {
147            return Ok(None);
148        };
149        let instance: ProviderInstance = serde_json::from_value(doc)
150            .with_context(|| format!("invalid provider instance `{provider_id}`"))?;
151        if !instance.enabled {
152            bail!("provider `{provider_id}` is disabled");
153        }
154        Ok(Some(binding_from_instance(instance)))
155    }
156}
157
158fn extract_inline_providers(manifest: &PackManifest) -> Result<Vec<ProviderExtDecl>> {
159    let Some(inline) = manifest.provider_extension_inline() else {
160        return Ok(Vec::new());
161    };
162
163    let providers = inline
164        .providers
165        .iter()
166        .map(|provider| ProviderExtDecl {
167            provider_id: Some(provider.provider_type.clone()),
168            provider_type: provider.provider_type.clone(),
169            capabilities: provider.capabilities.clone(),
170            ops: provider.ops.clone(),
171            config_schema_ref: Some(provider.config_schema_ref.clone()),
172            state_schema_ref: provider.state_schema_ref.clone(),
173            runtime: ProviderExtRuntime {
174                component_ref: provider.runtime.component_ref.clone(),
175                export: provider.runtime.export.clone(),
176                world: provider.runtime.world.clone(),
177            },
178            docs_ref: provider.docs_ref.clone(),
179        })
180        .collect();
181
182    Ok(providers)
183}
184
185fn binding_from_decl(
186    decl: &ProviderExtDecl,
187    pack_ref: Option<String>,
188    default_provider_id: Option<String>,
189) -> ProviderBinding {
190    ProviderBinding {
191        provider_id: decl.provider_id.clone().or(default_provider_id),
192        provider_type: decl.provider_type.clone(),
193        component_ref: decl.runtime.component_ref.clone(),
194        export: decl.runtime.export.clone(),
195        world: decl.runtime.world.clone(),
196        config_json: None,
197        pack_ref,
198    }
199}
200
201fn binding_from_instance(instance: ProviderInstance) -> ProviderBinding {
202    ProviderBinding {
203        config_json: if instance.config.is_null() {
204            None
205        } else {
206            Some(instance.config.to_string())
207        },
208        provider_id: Some(instance.provider_id),
209        provider_type: instance.provider_type,
210        component_ref: instance.component_ref,
211        export: instance.export,
212        world: instance.world,
213        pack_ref: instance.pack_ref,
214    }
215}