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}