#![allow(missing_docs)] use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub(super) mod triggers;
use asset_container::{AssetManager, Assets};
use tracing::trace;
use wick_asset_reference::{AssetReference, FetchOptions};
use wick_interface_types::TypeDefinition;
use wick_packet::{Entity, RuntimeConfig};
pub use self::triggers::*;
use super::common::component_definition::ComponentDefinition;
use super::common::package_definition::PackageConfig;
use super::components::TypesComponent;
use super::import_cache::{setup_cache, ImportCache};
use super::{Binding, ImportDefinition};
use crate::config::common::resources::*;
use crate::config::template_config::Renderable;
use crate::error::{ManifestError, ReferenceError};
use crate::lockdown::{validate_resource, FailureKind, Lockdown, LockdownError};
use crate::utils::{make_resolver, resolve, RwOption};
use crate::{config, ExpandImports, Resolver, Result};
#[derive(
Debug,
Clone,
Default,
derive_builder::Builder,
derive_asset_container::AssetManager,
property::Property,
serde::Serialize,
)]
#[property(get(public), set(public), mut(public, suffix = "_mut"))]
#[asset(asset(AssetReference))]
#[builder(
setter(into),
build_fn(name = "build_internal", private, error = "crate::error::BuilderError")
)]
#[must_use]
pub struct AppConfiguration {
#[asset(skip)]
pub(crate) name: String,
#[asset(skip)]
#[builder(setter(strip_option), default)]
#[property(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) source: Option<PathBuf>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) metadata: Option<config::Metadata>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) package: Option<PackageConfig>,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) import: Vec<Binding<ImportDefinition>>,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) resources: Vec<Binding<ResourceDefinition>>,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) triggers: Vec<TriggerDefinition>,
#[asset(skip)]
#[doc(hidden)]
#[builder(default)]
#[property(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) root_config: Option<RuntimeConfig>,
#[asset(skip)]
#[builder(default)]
#[property(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) env: Option<HashMap<String, String>>,
#[asset(skip)]
#[builder(setter(skip))]
#[property(skip)]
#[doc(hidden)]
#[serde(skip)]
pub(crate) type_cache: ImportCache,
#[asset(skip)]
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) options: Option<FetchOptions>,
#[asset(skip)]
#[builder(default)]
#[property(skip)]
#[doc(hidden)]
#[serde(skip)]
pub(crate) cached_types: RwOption<Vec<TypeDefinition>>,
}
impl AppConfiguration {
pub(crate) async fn setup_cache(&self, options: FetchOptions) -> Result<()> {
setup_cache(
&self.type_cache,
self.import.iter(),
&self.cached_types,
vec![],
options,
)
.await
}
pub fn package_files(&self) -> Assets<AssetReference> {
self.package.assets()
}
#[must_use]
pub fn resolve_type(&self, name: &str) -> Option<TypeDefinition> {
self
.cached_types
.read()
.as_ref()
.and_then(|types| types.iter().find(|t| t.name() == name).cloned())
}
pub fn resolve_binding(&self, name: &str) -> Result<OwnedConfigurationItem> {
resolve(name, &self.import, &self.resources)
}
#[must_use]
pub fn resolver(&self) -> Box<Resolver> {
make_resolver(self.import.clone(), self.resources.clone())
}
#[must_use]
pub fn source(&self) -> Option<&Path> {
self.source.as_deref()
}
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 version(&self) -> Option<&str> {
self.metadata.as_ref().map(|m| m.version.as_str())
}
pub fn add_resource<T: Into<String>>(&mut self, name: T, resource: ResourceDefinition) {
self.resources.push(Binding::new(name, resource));
}
#[cfg(feature = "v1")]
pub fn into_v1_yaml(self) -> Result<String> {
let v1_manifest: crate::v1::AppConfiguration = self.try_into()?;
Ok(serde_yaml::to_string(&v1_manifest).unwrap())
}
pub(super) 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 app resources"
);
let env = self.env.as_ref();
let mut bindings = Vec::new();
for (i, trigger) in self.triggers.iter_mut().enumerate() {
trigger.expand_imports(&mut bindings, i)?;
}
self.import.extend(bindings);
self.resources.render_config(source.as_deref(), root_config, env)?;
self.import.render_config(source.as_deref(), root_config, env)?;
self.triggers.render_config(source.as_deref(), root_config, env)?;
Ok(self)
}
pub const fn validate(&self) -> Result<()> {
Ok(())
}
}
impl Renderable for AppConfiguration {
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)?;
self.triggers.render_config(source, root_config, env)?;
Ok(())
}
}
impl Lockdown for AppConfiguration {
fn lockdown(
&self,
_id: Option<&str>,
lockdown: &config::LockdownConfiguration,
) -> std::result::Result<(), LockdownError> {
let mut errors = Vec::new();
let 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))
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub enum ConfigurationItem<'a> {
Component(&'a ComponentDefinition),
Types(&'a TypesComponent),
Resource(&'a ResourceDefinition),
}
impl<'a> ConfigurationItem<'a> {
pub const fn try_component(&self) -> std::result::Result<&'a ComponentDefinition, ReferenceError> {
match self {
Self::Component(c) => Ok(c),
_ => Err(ReferenceError::Component),
}
}
pub const fn try_types(&self) -> std::result::Result<&'a TypesComponent, ReferenceError> {
match self {
Self::Types(c) => Ok(c),
_ => Err(ReferenceError::Types),
}
}
pub const fn try_resource(&self) -> std::result::Result<&'a ResourceDefinition, ReferenceError> {
match self {
Self::Resource(c) => Ok(c),
_ => Err(ReferenceError::Resource),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub enum OwnedConfigurationItem {
Component(ComponentDefinition),
Resource(ResourceDefinition),
}
impl OwnedConfigurationItem {
#[allow(clippy::missing_const_for_fn)]
pub fn try_component(self) -> Result<ComponentDefinition> {
match self {
Self::Component(c) => Ok(c),
_ => Err(ManifestError::Reference(ReferenceError::Component)),
}
}
#[allow(clippy::missing_const_for_fn)]
pub fn try_resource(self) -> Result<ResourceDefinition> {
match self {
Self::Resource(c) => Ok(c),
_ => Err(ManifestError::Reference(ReferenceError::Resource)),
}
}
}
impl AppConfigurationBuilder {
pub fn build(&self) -> Result<AppConfiguration> {
let config = self.clone();
let config = config.build_internal()?;
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use serde_json::json;
use super::*;
use crate::config::components::ManifestComponentBuilder;
use crate::config::{Codec, ComponentOperationExpressionBuilder, LiquidJsonConfig, MiddlewareBuilder};
#[test]
fn test_trigger_render() -> Result<()> {
let op = ComponentOperationExpressionBuilder::default()
.component(ComponentDefinition::Manifest(
ManifestComponentBuilder::default()
.reference("this/that:0.0.1")
.build()?,
))
.config(LiquidJsonConfig::try_from(
json!({"op_config_field": "{{ctx.env.CARGO_MANIFEST_DIR}}"}),
)?)
.name("test")
.build()?;
let trigger = HttpTriggerConfigBuilder::default()
.resource("URL")
.routers(vec![HttpRouterConfig::RawRouter(RawRouterConfig {
path: "/".to_owned(),
middleware: Some(
MiddlewareBuilder::default()
.request(vec![op.clone()])
.response(vec![op.clone()])
.build()?,
),
codec: Some(Codec::Json),
operation: op,
})])
.build()?;
let mut config = AppConfigurationBuilder::default()
.name("test")
.resources(vec![Binding::new("PORT", TcpPort::new("0.0.0.0", 90))])
.triggers(vec![TriggerDefinition::Http(trigger)])
.build()?;
config.env = Some(std::env::vars().collect());
config.root_config = Some(json!({"config_val": "from_config"}).try_into()?);
config.initialize()?;
let TriggerDefinition::Http(mut trigger) = config.triggers.pop().unwrap() else {
unreachable!();
};
let HttpRouterConfig::RawRouter(mut router) = trigger.routers.pop().unwrap() else {
unreachable!();
};
let cargo_manifest_dir = json!(env!("CARGO_MANIFEST_DIR"));
let config = router.operation.config.take().unwrap().value.unwrap();
assert_eq!(config.get("op_config_field"), Some(&cargo_manifest_dir));
let mut mw = router.middleware.take().unwrap();
let mw_req_config = mw.request[0].config.take().unwrap().value.unwrap();
assert_eq!(mw_req_config.get("op_config_field"), Some(&cargo_manifest_dir));
let mw_res_config = mw.response[0].config.take().unwrap().value.unwrap();
assert_eq!(mw_res_config.get("op_config_field"), Some(&cargo_manifest_dir));
Ok(())
}
}