#![allow(missing_docs)] mod composite;
mod wasm;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use asset_container::{AssetManager, Assets};
pub use composite::*;
use config::{ComponentImplementation, ComponentKind};
use tracing::trace;
pub use wasm::*;
use wick_asset_reference::{AssetReference, FetchOptions};
use wick_interface_types::{ComponentMetadata, ComponentSignature, Field, OperationSignature, TypeDefinition};
use wick_packet::{Entity, RuntimeConfig};
use super::common::package_definition::PackageConfig;
use super::import_cache::{setup_cache, ImportCache};
use super::{Binding, ImportDefinition, InterfaceDefinition, ResourceDefinition, TestConfiguration};
use crate::config::template_config::Renderable;
use crate::lockdown::{validate_resource, FailureKind, Lockdown, LockdownError};
use crate::utils::{make_resolver, RwOption};
use crate::{config, Error, Resolver, Result};
#[derive(
Debug,
Default,
Clone,
derive_builder::Builder,
derive_asset_container::AssetManager,
property::Property,
serde::Serialize,
)]
#[builder(
derive(Debug),
setter(into),
build_fn(name = "build_internal", private, error = "crate::error::BuilderError")
)]
#[property(get(public), set(public), mut(public, suffix = "_mut"))]
#[asset(asset(AssetReference))]
#[must_use]
pub struct ComponentConfiguration {
#[asset(skip)]
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) name: Option<String>,
pub(crate) component: ComponentImplementation,
#[asset(skip)]
#[builder(setter(strip_option), default)]
#[property(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) source: Option<PathBuf>,
#[asset(skip)]
#[builder(default)]
#[property(skip)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) types: Vec<TypeDefinition>,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) import: Vec<Binding<ImportDefinition>>,
#[asset(skip)]
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) requires: Vec<Binding<InterfaceDefinition>>,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) resources: Vec<Binding<ResourceDefinition>>,
#[asset(skip)]
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) host: Option<config::HostConfig>,
#[asset(skip)]
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) tests: Vec<TestConfiguration>,
#[asset(skip)]
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) metadata: Option<config::Metadata>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) package: Option<PackageConfig>,
#[asset(skip)]
#[doc(hidden)]
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) root_config: Option<RuntimeConfig>,
#[asset(skip)]
#[builder(setter(skip))]
#[property(skip)]
#[doc(hidden)]
#[serde(skip)]
pub(crate) type_cache: ImportCache,
#[asset(skip)]
#[builder(setter(skip))]
#[property(skip)]
#[doc(hidden)]
#[serde(skip)]
pub(crate) cached_types: RwOption<Vec<TypeDefinition>>,
}
impl ComponentConfiguration {
pub const fn try_composite(&self) -> Result<&CompositeComponentImplementation> {
match &self.component {
ComponentImplementation::Composite(c) => Ok(c),
_ => Err(Error::UnexpectedComponentType(
ComponentKind::Composite,
self.component.kind(),
)),
}
}
pub const fn try_wasm(&self) -> Result<&WasmComponentImplementation> {
match &self.component {
ComponentImplementation::Wasm(c) => Ok(c),
_ => Err(Error::UnexpectedComponentType(
ComponentKind::Wasm,
self.component.kind(),
)),
}
}
#[must_use]
pub fn package_files(&self) -> Option<Assets<AssetReference>> {
self.package.as_ref().map(|p| p.assets())
}
pub fn set_source(&mut self, source: &Path) {
let source = source.to_path_buf();
self.source = Some(source);
}
pub(super) fn update_baseurls(&self) {
#[allow(clippy::expect_used)]
let mut source = self.source.clone().expect("No source set for this configuration");
if !source.is_dir() {
source.pop();
}
self.set_baseurl(&source);
}
#[must_use]
pub fn resolver(&self) -> Box<Resolver> {
make_resolver(self.import.clone(), self.resources.clone())
}
pub const fn kind(&self) -> ComponentKind {
self.component.kind()
}
#[must_use]
pub fn allow_latest(&self) -> bool {
self.host.as_ref().map_or(false, |v| v.allow_latest)
}
#[must_use]
pub fn insecure_registries(&self) -> Option<&[String]> {
self.host.as_ref().map(|v| v.insecure_registries.as_ref())
}
#[must_use]
pub fn version(&self) -> Option<&str> {
self.metadata.as_ref().map(|m| m.version.as_str())
}
#[must_use]
pub fn source(&self) -> Option<&Path> {
self.source.as_deref()
}
pub fn types(&self) -> Result<Vec<TypeDefinition>> {
self.cached_types.read().as_ref().map_or_else(
|| {
if self.import.is_empty() {
Ok(self.types.clone())
} else {
Err(Error::TypesNotFetched)
}
},
|types| Ok(types.clone()),
)
}
pub fn types_mut(&mut self) -> &mut Vec<TypeDefinition> {
&mut self.types
}
pub(crate) async fn setup_cache(&self, options: FetchOptions) -> Result<()> {
setup_cache(
&self.type_cache,
self.import.iter(),
&self.cached_types,
self.types.clone(),
options,
)
.await
}
#[must_use]
pub fn config(&self) -> &[Field] {
match &self.component {
ComponentImplementation::Composite(c) => &c.config,
ComponentImplementation::Wasm(c) => &c.config,
ComponentImplementation::Sql(c) => &c.config,
ComponentImplementation::HttpClient(c) => &c.config,
}
}
pub fn signature(&self) -> Result<ComponentSignature> {
let mut sig = wick_interface_types::component! {
name: self.name().cloned().unwrap_or_else(||self.component.default_name().to_owned()),
version: self.version(),
operations: self.component.operation_signatures(),
};
sig.config = self.config().to_vec();
sig.types = self.types()?;
Ok(sig)
}
#[cfg(feature = "v1")]
pub fn into_v1_yaml(self) -> Result<String> {
let v1_manifest: crate::v1::ComponentConfiguration = self.try_into()?;
Ok(serde_yaml::to_string(&v1_manifest).unwrap())
}
pub fn initialize(&mut self) -> Result<&Self> {
let root_config = self.root_config.as_ref();
let source = self.source().map(std::path::Path::to_path_buf);
trace!(
source = ?source,
num_resources = self.resources.len(),
num_imports = self.import.len(),
?root_config,
"initializing component"
);
self.resources.render_config(source.as_deref(), root_config, None)?;
self.import.render_config(source.as_deref(), root_config, None)?;
Ok(self)
}
pub fn validate(&self) -> Result<()> {
wick_packet::validation::expect_configuration_matches(
self.source().map_or("<unknown>", |p| p.to_str().unwrap_or("<invalid>")),
self.root_config.as_ref(),
self.config(),
)
.map_err(Error::ConfigurationInvalid)?;
Ok(())
}
}
impl Renderable for ComponentConfiguration {
fn render_config(
&mut self,
source: Option<&Path>,
root_config: Option<&RuntimeConfig>,
env: Option<&HashMap<String, String>>,
) -> Result<()> {
self.resources.render_config(source, root_config, env)?;
self.import.render_config(source, root_config, env)?;
Ok(())
}
}
impl Lockdown for ComponentConfiguration {
fn lockdown(
&self,
id: Option<&str>,
lockdown: &config::LockdownConfiguration,
) -> std::result::Result<(), LockdownError> {
let mut errors = Vec::new();
let Some(id) = id else {
return Err(LockdownError::new(vec![FailureKind::General(
"missing component id".into(),
)]));
};
if id == Entity::LOCAL {
return Err(LockdownError::new(vec![FailureKind::General(format!(
"invalid component id: {}",
Entity::LOCAL
))]));
}
for resource in &self.resources {
if let Err(e) = validate_resource(id, &(resource.into()), lockdown) {
errors.push(FailureKind::Failed(Box::new(e)));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(LockdownError::new(errors))
}
}
}
impl ComponentConfigurationBuilder {
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn from_base(config: ComponentConfiguration) -> Self {
Self {
name: Some(config.name),
component: Some(config.component),
source: None,
types: Some(config.types),
import: Some(config.import),
requires: Some(config.requires),
resources: Some(config.resources),
host: Some(config.host),
tests: Some(config.tests),
metadata: Some(config.metadata),
package: Some(config.package),
root_config: Some(config.root_config),
type_cache: std::marker::PhantomData,
cached_types: std::marker::PhantomData,
}
}
pub fn add_import(&mut self, import: Binding<ImportDefinition>) {
if let Some(imports) = &mut self.import {
imports.push(import);
} else {
self.import = Some(vec![import]);
}
}
pub fn add_resource(&mut self, resource: Binding<ResourceDefinition>) {
if let Some(r) = &mut self.resources {
r.push(resource);
} else {
self.resources = Some(vec![resource]);
}
}
pub fn build(self) -> Result<ComponentConfiguration> {
let config = self.build_internal()?;
config.validate()?;
Ok(config)
}
}
impl From<config::Metadata> for ComponentMetadata {
fn from(value: config::Metadata) -> Self {
Self::new(Some(value.version))
}
}
impl From<config::OperationDefinition> for OperationSignature {
fn from(value: config::OperationDefinition) -> Self {
Self::new(value.name, value.inputs, value.outputs, value.config)
}
}