1#![allow(missing_docs)] mod composite;
3mod wasm_component_model;
4mod wasmrs;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use asset_container::{AssetManager, Assets};
9pub use composite::*;
10use config::{ComponentImplementation, ComponentKind};
11use tracing::trace;
12pub use wasm_component_model::{
13 WasmComponentDefinition,
14 WasmComponentDefinitionBuilder,
15 WasmComponentDefinitionBuilderError,
16};
17pub use wasmrs::{WasmRsComponent, WasmRsComponentBuilder, WasmRsComponentBuilderError};
18use wick_asset_reference::{AssetReference, FetchOptions};
19use wick_interface_types::{ComponentMetadata, ComponentSignature, Field, OperationSignature, TypeDefinition};
20use wick_packet::{Entity, RuntimeConfig};
21
22use super::common::package_definition::PackageConfig;
23use super::import_cache::{setup_cache, ImportCache};
24use super::{Binding, ImportDefinition, InterfaceDefinition, ResourceDefinition, TestConfiguration};
25use crate::config::template_config::Renderable;
26use crate::lockdown::{validate_resource, FailureKind, Lockdown, LockdownError};
27use crate::utils::{make_resolver, RwOption};
28use crate::{config, Error, Resolver, Result};
29
30#[derive(
31 Debug,
32 Default,
33 Clone,
34 derive_builder::Builder,
35 derive_asset_container::AssetManager,
36 property::Property,
37 serde::Serialize,
38)]
39#[builder(
40 derive(Debug),
41 setter(into),
42 build_fn(name = "build_internal", private, error = "crate::error::BuilderError")
43)]
44#[property(get(public), set(public), mut(public, suffix = "_mut"))]
45#[asset(asset(AssetReference))]
46#[must_use]
47pub struct ComponentConfiguration {
52 #[asset(skip)]
53 #[builder(setter(strip_option), default)]
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub(crate) name: Option<String>,
57
58 pub(crate) component: ComponentImplementation,
60
61 #[asset(skip)]
62 #[builder(setter(strip_option), default)]
63 #[property(skip)]
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub(crate) source: Option<PathBuf>,
67
68 #[asset(skip)]
69 #[builder(default)]
70 #[property(skip)]
71 #[serde(skip_serializing_if = "Vec::is_empty")]
73 pub(crate) types: Vec<TypeDefinition>,
74
75 #[builder(default)]
76 #[serde(skip_serializing_if = "Vec::is_empty")]
78 pub(crate) import: Vec<Binding<ImportDefinition>>,
79
80 #[asset(skip)]
81 #[builder(default)]
82 #[serde(skip_serializing_if = "Vec::is_empty")]
84 pub(crate) requires: Vec<Binding<InterfaceDefinition>>,
85
86 #[builder(default)]
87 #[serde(skip_serializing_if = "Vec::is_empty")]
89 pub(crate) resources: Vec<Binding<ResourceDefinition>>,
90
91 #[asset(skip)]
92 #[builder(default)]
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub(crate) host: Option<config::HostConfig>,
96
97 #[asset(skip)]
98 #[builder(default)]
99 #[serde(skip_serializing_if = "Vec::is_empty")]
101 pub(crate) tests: Vec<TestConfiguration>,
102
103 #[asset(skip)]
104 #[builder(default)]
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub(crate) metadata: Option<config::Metadata>,
108
109 #[builder(default)]
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub(crate) package: Option<PackageConfig>,
113
114 #[asset(skip)]
115 #[doc(hidden)]
116 #[builder(default)]
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub(crate) root_config: Option<RuntimeConfig>,
119
120 #[asset(skip)]
121 #[builder(setter(skip))]
122 #[property(skip)]
123 #[doc(hidden)]
124 #[serde(skip)]
125 pub(crate) type_cache: ImportCache,
126
127 #[asset(skip)]
128 #[builder(setter(skip))]
129 #[property(skip)]
130 #[doc(hidden)]
131 #[serde(skip)]
132 pub(crate) cached_types: RwOption<Vec<TypeDefinition>>,
133}
134
135impl ComponentConfiguration {
136 pub const fn try_composite(&self) -> Result<&CompositeComponentImplementation> {
138 match &self.component {
139 ComponentImplementation::Composite(c) => Ok(c),
140 _ => Err(Error::UnexpectedComponentType(
141 ComponentKind::Composite,
142 self.component.kind(),
143 )),
144 }
145 }
146
147 pub const fn try_wasmrs(&self) -> Result<&WasmRsComponent> {
149 match &self.component {
150 ComponentImplementation::WasmRs(c) => Ok(c),
151 _ => Err(Error::UnexpectedComponentType(
152 ComponentKind::WasmRs,
153 self.component.kind(),
154 )),
155 }
156 }
157
158 #[must_use]
160 pub fn package_files(&self) -> Option<Assets<AssetReference>> {
161 self.package.as_ref().map(|p| p.assets())
163 }
164
165 pub fn set_source(&mut self, source: &Path) {
167 let source = source.to_path_buf();
168 self.source = Some(source);
169 }
170
171 pub(super) fn update_baseurls(&self) {
172 #[allow(clippy::expect_used)]
173 let mut source = self.source.clone().expect("No source set for this configuration");
174 if !source.is_dir() {
176 source.pop();
177 }
178
179 self.set_baseurl(&source);
180 }
181
182 #[must_use]
184 pub fn resolver(&self) -> Box<Resolver> {
185 make_resolver(self.import.clone(), self.resources.clone())
186 }
187
188 pub const fn kind(&self) -> ComponentKind {
190 self.component.kind()
191 }
192
193 #[must_use]
195 pub fn allow_latest(&self) -> bool {
196 self.host.as_ref().map_or(false, |v| v.allow_latest)
197 }
198
199 #[must_use]
201 pub fn insecure_registries(&self) -> Option<&[String]> {
202 self.host.as_ref().map(|v| v.insecure_registries.as_ref())
203 }
204
205 #[must_use]
207 pub fn version(&self) -> Option<&str> {
208 self.metadata.as_ref().map(|m| m.version.as_str())
209 }
210
211 #[must_use]
213 pub fn source(&self) -> Option<&Path> {
214 self.source.as_deref()
215 }
216
217 pub fn types(&self) -> Result<Vec<TypeDefinition>> {
219 self.cached_types.read().as_ref().map_or_else(
220 || {
221 if self.import.is_empty() {
222 Ok(self.types.clone())
223 } else {
224 Err(Error::TypesNotFetched)
225 }
226 },
227 |types| Ok(types.clone()),
228 )
229 }
230
231 pub fn types_mut(&mut self) -> &mut Vec<TypeDefinition> {
233 &mut self.types
234 }
235
236 pub(crate) async fn setup_cache(&self, options: FetchOptions) -> Result<()> {
238 setup_cache(
239 &self.type_cache,
240 self.import.iter(),
241 &self.cached_types,
242 self.types.clone(),
243 options,
244 )
245 .await
246 }
247
248 #[must_use]
249 pub fn config(&self) -> &[Field] {
250 match &self.component {
251 ComponentImplementation::Composite(c) => &c.config,
252 ComponentImplementation::Wasm(c) => &c.config,
253 ComponentImplementation::WasmRs(c) => &c.config,
254 ComponentImplementation::Sql(c) => &c.config,
255 ComponentImplementation::HttpClient(c) => &c.config,
256 }
257 }
258
259 pub fn signature(&self) -> Result<ComponentSignature> {
261 let mut sig = wick_interface_types::component! {
262 name: self.name().cloned().unwrap_or_else(||self.component.default_name().to_owned()),
263 version: self.version(),
264 operations: self.component.operation_signatures(),
265 };
266 sig.config = self.config().to_vec();
267 sig.types = self.types()?;
268 Ok(sig)
269 }
270
271 #[cfg(feature = "v1")]
273 pub fn into_v1_yaml(self) -> Result<String> {
274 let v1_manifest: crate::v1::ComponentConfiguration = self.try_into()?;
275 Ok(serde_yaml::to_string(&v1_manifest).unwrap())
276 }
277
278 pub fn initialize(&mut self) -> Result<&Self> {
280 let root_config = self.root_config.as_ref();
281 let source = self.source().map(std::path::Path::to_path_buf);
282 trace!(
283 source = ?source,
284 num_resources = self.resources.len(),
285 num_imports = self.import.len(),
286 ?root_config,
287 "initializing component"
288 );
289
290 self.resources.render_config(source.as_deref(), root_config, None)?;
291 self.import.render_config(source.as_deref(), root_config, None)?;
292
293 Ok(self)
294 }
295
296 pub fn validate(&self) -> Result<()> {
298 wick_packet::validation::expect_configuration_matches(
299 self.source().map_or("<unknown>", |p| p.to_str().unwrap_or("<invalid>")),
300 self.root_config.as_ref(),
301 self.config(),
302 )
303 .map_err(Error::ConfigurationInvalid)?;
304 Ok(())
305 }
306}
307
308impl Renderable for ComponentConfiguration {
309 fn render_config(
310 &mut self,
311 source: Option<&Path>,
312 root_config: Option<&RuntimeConfig>,
313 env: Option<&HashMap<String, String>>,
314 ) -> Result<()> {
315 self.resources.render_config(source, root_config, env)?;
316 self.import.render_config(source, root_config, env)?;
317 Ok(())
318 }
319}
320
321impl Lockdown for ComponentConfiguration {
322 fn lockdown(
323 &self,
324 id: Option<&str>,
325 lockdown: &config::LockdownConfiguration,
326 ) -> std::result::Result<(), LockdownError> {
327 let mut errors = Vec::new();
328 let Some(id) = id else {
329 return Err(LockdownError::new(vec![FailureKind::General(
330 "missing component id".into(),
331 )]));
332 };
333
334 if id == Entity::LOCAL {
335 return Err(LockdownError::new(vec![FailureKind::General(format!(
336 "invalid component id: {}",
337 Entity::LOCAL
338 ))]));
339 }
340
341 for resource in &self.resources {
342 if let Err(e) = validate_resource(id, &(resource.into()), lockdown) {
343 errors.push(FailureKind::Failed(Box::new(e)));
344 }
345 }
346
347 if errors.is_empty() {
348 Ok(())
349 } else {
350 Err(LockdownError::new(errors))
351 }
352 }
353}
354
355impl ComponentConfigurationBuilder {
356 #[must_use]
357 #[allow(clippy::missing_const_for_fn)]
359 pub fn from_base(config: ComponentConfiguration) -> Self {
360 Self {
361 name: Some(config.name),
362 component: Some(config.component),
363 source: None,
364 types: Some(config.types),
365 import: Some(config.import),
366 requires: Some(config.requires),
367 resources: Some(config.resources),
368 host: Some(config.host),
369 tests: Some(config.tests),
370 metadata: Some(config.metadata),
371 package: Some(config.package),
372 root_config: Some(config.root_config),
373 type_cache: std::marker::PhantomData,
374 cached_types: std::marker::PhantomData,
375 }
376 }
377
378 pub fn add_import(&mut self, import: Binding<ImportDefinition>) {
380 if let Some(imports) = &mut self.import {
381 imports.push(import);
382 } else {
383 self.import = Some(vec![import]);
384 }
385 }
386
387 pub fn add_resource(&mut self, resource: Binding<ResourceDefinition>) {
389 if let Some(r) = &mut self.resources {
390 r.push(resource);
391 } else {
392 self.resources = Some(vec![resource]);
393 }
394 }
395
396 pub fn build(self) -> Result<ComponentConfiguration> {
398 let config = self.build_internal()?;
399 config.validate()?;
400 Ok(config)
401 }
402}
403
404impl From<config::Metadata> for ComponentMetadata {
405 fn from(value: config::Metadata) -> Self {
406 Self::new(Some(value.version))
407 }
408}
409
410impl From<config::OperationDefinition> for OperationSignature {
411 fn from(value: config::OperationDefinition) -> Self {
412 Self::new(value.name, value.inputs, value.outputs, value.config)
413 }
414}