wick_config/config/
app_config.rs

1#![allow(missing_docs)] // delete when we move away from the `property` crate.
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4pub(super) mod triggers;
5
6use asset_container::{AssetManager, Assets};
7use tracing::trace;
8use wick_asset_reference::{AssetReference, FetchOptions};
9use wick_interface_types::TypeDefinition;
10use wick_packet::{Entity, RuntimeConfig};
11
12pub use self::triggers::*;
13use super::common::component_definition::ComponentDefinition;
14use super::common::package_definition::PackageConfig;
15use super::components::TypesComponent;
16use super::import_cache::{setup_cache, ImportCache};
17use super::{Binding, BoundIdentifier, ImportDefinition};
18use crate::config::common::resources::*;
19use crate::config::template_config::Renderable;
20use crate::error::{ManifestError, ReferenceError};
21use crate::lockdown::{validate_resource, FailureKind, Lockdown, LockdownError};
22use crate::utils::{make_resolver, resolve, RwOption};
23use crate::{config, ExpandImports, Resolver, Result};
24
25#[derive(
26  Debug,
27  Clone,
28  Default,
29  derive_builder::Builder,
30  derive_asset_container::AssetManager,
31  property::Property,
32  serde::Serialize,
33)]
34#[property(get(public), set(public), mut(public, suffix = "_mut"))]
35#[asset(asset(AssetReference))]
36#[builder(
37  setter(into),
38  build_fn(name = "build_internal", private, error = "crate::error::BuilderError")
39)]
40#[must_use]
41/// A Wick application configuration.
42///
43/// An application configuration defines a wick application, its trigger, imported component, etc and can be executed
44/// via `wick run`.
45pub struct AppConfiguration {
46  #[asset(skip)]
47  /// The name of the application.
48  pub(crate) name: String,
49
50  #[asset(skip)]
51  #[builder(setter(strip_option), default)]
52  #[property(skip)]
53  /// The source (i.e. url or file on disk) of the configuration.
54  #[serde(skip_serializing_if = "Option::is_none")]
55  pub(crate) source: Option<PathBuf>,
56
57  #[builder(setter(strip_option), default)]
58  /// The metadata for the application.
59  #[serde(skip_serializing_if = "Option::is_none")]
60  pub(crate) metadata: Option<config::Metadata>,
61
62  #[builder(setter(strip_option), default)]
63  /// The package configuration for this application.
64  #[serde(skip_serializing_if = "Option::is_none")]
65  pub(crate) package: Option<PackageConfig>,
66
67  #[builder(default)]
68  /// The components that make up the application.
69  #[serde(skip_serializing_if = "Vec::is_empty")]
70  pub(crate) import: Vec<Binding<ImportDefinition>>,
71
72  #[builder(default)]
73  /// Any resources this application defines.
74  #[serde(skip_serializing_if = "Vec::is_empty")]
75  pub(crate) resources: Vec<Binding<ResourceDefinition>>,
76
77  #[builder(default)]
78  /// The triggers that initialize upon a `run` and make up the application.
79  #[serde(skip_serializing_if = "Vec::is_empty")]
80  pub(crate) triggers: Vec<TriggerDefinition>,
81
82  #[asset(skip)]
83  #[doc(hidden)]
84  #[builder(default)]
85  #[property(skip)]
86  #[serde(skip_serializing_if = "Option::is_none")]
87  pub(crate) root_config: Option<RuntimeConfig>,
88
89  #[asset(skip)]
90  #[builder(default)]
91  #[property(skip)]
92  /// The environment this configuration has access to.
93  #[serde(skip_serializing_if = "Option::is_none")]
94  pub(crate) env: Option<HashMap<String, String>>,
95
96  #[asset(skip)]
97  #[builder(setter(skip))]
98  #[property(skip)]
99  #[doc(hidden)]
100  #[serde(skip)]
101  pub(crate) type_cache: ImportCache,
102
103  #[asset(skip)]
104  #[builder(default)]
105  #[serde(skip_serializing_if = "Option::is_none")]
106  pub(crate) options: Option<FetchOptions>,
107
108  #[asset(skip)]
109  #[builder(default)]
110  #[property(skip)]
111  #[doc(hidden)]
112  #[serde(skip)]
113  pub(crate) cached_types: RwOption<Vec<TypeDefinition>>,
114}
115
116impl AppConfiguration {
117  /// Fetch/cache anything critical to the first use of this configuration.
118  pub(crate) async fn setup_cache(&self, options: FetchOptions) -> Result<()> {
119    setup_cache(
120      &self.type_cache,
121      self.import.iter(),
122      &self.cached_types,
123      vec![],
124      options,
125    )
126    .await
127  }
128
129  /// Get the package files
130  pub fn package_files(&self) -> Assets<AssetReference> {
131    self.package.assets()
132  }
133
134  /// Resolve an imported type by name.
135  #[must_use]
136  pub fn resolve_type(&self, name: &str) -> Option<TypeDefinition> {
137    self
138      .cached_types
139      .read()
140      .as_ref()
141      .and_then(|types| types.iter().find(|t| t.name() == name).cloned())
142  }
143
144  /// Get the configuration item a binding points to.
145
146  pub fn resolve_binding(&self, name: &BoundIdentifier) -> Result<OwnedConfigurationItem> {
147    resolve(name, &self.import, &self.resources)
148  }
149
150  /// Returns a function that resolves a binding to a configuration item.
151  #[must_use]
152  pub fn resolver(&self) -> Box<Resolver> {
153    make_resolver(self.import.clone(), self.resources.clone())
154  }
155
156  /// Return the underlying version of the source manifest.
157  #[must_use]
158  pub fn source(&self) -> Option<&Path> {
159    self.source.as_deref()
160  }
161
162  /// Set the source location of the configuration.
163  pub fn set_source(&mut self, source: &Path) {
164    let source = source.to_path_buf();
165    self.source = Some(source);
166  }
167
168  pub(super) fn update_baseurls(&self) {
169    #[allow(clippy::expect_used)]
170    let mut source = self.source.clone().expect("No source set for this configuration");
171    // Source is (should be) a file, so pop the filename before setting the baseurl.
172    if !source.is_dir() {
173      source.pop();
174    }
175    self.set_baseurl(&source);
176  }
177
178  /// Return the version of the application.
179  #[must_use]
180  pub fn version(&self) -> Option<&str> {
181    self.metadata.as_ref().map(|m| m.version.as_str())
182  }
183
184  /// Add a resource to the application configuration.
185  pub fn add_resource<T: Into<String>>(&mut self, name: T, resource: ResourceDefinition) {
186    self.resources.push(Binding::new(name, resource));
187  }
188
189  /// Generate V1 configuration yaml from this configuration.
190  #[cfg(feature = "v1")]
191  pub fn into_v1_yaml(self) -> Result<String> {
192    let v1_manifest: crate::v1::AppConfiguration = self.try_into()?;
193    Ok(serde_yaml::to_string(&v1_manifest).unwrap())
194  }
195
196  /// Initialize the configuration with the given environment variables.
197  pub(super) fn initialize(&mut self) -> Result<&Self> {
198    let root_config = self.root_config.as_ref();
199    let source = self.source().map(std::path::Path::to_path_buf);
200
201    trace!(
202      source = ?source,
203      num_resources = self.resources.len(),
204      num_imports = self.import.len(),
205      ?root_config,
206      "initializing app resources"
207    );
208    let env = self.env.as_ref();
209
210    let mut bindings = Vec::new();
211    for (i, trigger) in self.triggers.iter_mut().enumerate() {
212      trigger.expand_imports(&mut bindings, i)?;
213    }
214    self.import.extend(bindings);
215
216    self.resources.render_config(source.as_deref(), root_config, env)?;
217    self.import.render_config(source.as_deref(), root_config, env)?;
218    self.triggers.render_config(source.as_deref(), root_config, env)?;
219
220    Ok(self)
221  }
222
223  /// Validate this configuration is good.
224  pub const fn validate(&self) -> Result<()> {
225    /* placeholder */
226    Ok(())
227  }
228}
229
230impl Renderable for AppConfiguration {
231  fn render_config(
232    &mut self,
233    source: Option<&Path>,
234    root_config: Option<&RuntimeConfig>,
235    env: Option<&HashMap<String, String>>,
236  ) -> Result<()> {
237    self.resources.render_config(source, root_config, env)?;
238    self.import.render_config(source, root_config, env)?;
239    self.triggers.render_config(source, root_config, env)?;
240    Ok(())
241  }
242}
243
244impl Lockdown for AppConfiguration {
245  fn lockdown(
246    &self,
247    _id: Option<&str>,
248    lockdown: &config::LockdownConfiguration,
249  ) -> std::result::Result<(), LockdownError> {
250    let mut errors = Vec::new();
251    let id = Entity::LOCAL;
252
253    for resource in &self.resources {
254      if let Err(e) = validate_resource(id, &(resource.into()), lockdown) {
255        errors.push(FailureKind::Failed(Box::new(e)));
256      }
257    }
258    if errors.is_empty() {
259      Ok(())
260    } else {
261      Err(LockdownError::new(errors))
262    }
263  }
264}
265
266/// A configuration item
267#[derive(Debug, Clone, PartialEq)]
268#[must_use]
269
270pub enum ConfigurationItem<'a> {
271  /// A component definition.
272  Component(&'a ComponentDefinition),
273  /// A component definition.
274  Types(&'a TypesComponent),
275  /// A resource definition.
276  Resource(&'a ResourceDefinition),
277}
278
279impl<'a> ConfigurationItem<'a> {
280  /// Get the component definition or return an error.
281  pub const fn try_component(&self) -> std::result::Result<&'a ComponentDefinition, ReferenceError> {
282    match self {
283      Self::Component(c) => Ok(c),
284      _ => Err(ReferenceError::Component),
285    }
286  }
287
288  /// Get the types definition or return an error.
289  pub const fn try_types(&self) -> std::result::Result<&'a TypesComponent, ReferenceError> {
290    match self {
291      Self::Types(c) => Ok(c),
292      _ => Err(ReferenceError::Types),
293    }
294  }
295
296  /// Get the resource definition or return an error.
297  pub const fn try_resource(&self) -> std::result::Result<&'a ResourceDefinition, ReferenceError> {
298    match self {
299      Self::Resource(c) => Ok(c),
300      _ => Err(ReferenceError::Resource),
301    }
302  }
303}
304
305/// A configuration item
306#[derive(Debug, Clone, PartialEq)]
307#[must_use]
308
309pub enum OwnedConfigurationItem {
310  /// A component definition.
311  Component(ComponentDefinition),
312  /// A resource definition.
313  Resource(ResourceDefinition),
314}
315
316impl OwnedConfigurationItem {
317  /// Get the component definition or return an error.
318  #[allow(clippy::missing_const_for_fn)]
319  pub fn try_component(self) -> Result<ComponentDefinition> {
320    match self {
321      Self::Component(c) => Ok(c),
322      _ => Err(ManifestError::Reference(ReferenceError::Component)),
323    }
324  }
325  /// Get the resource definition or return an error.
326  #[allow(clippy::missing_const_for_fn)]
327  pub fn try_resource(self) -> Result<ResourceDefinition> {
328    match self {
329      Self::Resource(c) => Ok(c),
330      _ => Err(ManifestError::Reference(ReferenceError::Resource)),
331    }
332  }
333}
334
335impl AppConfigurationBuilder {
336  /// Build the configuration.
337  pub fn build(&self) -> Result<AppConfiguration> {
338    let config = self.clone();
339    let config = config.build_internal()?;
340    config.validate()?;
341    Ok(config)
342  }
343}
344
345#[cfg(test)]
346mod test {
347  use anyhow::Result;
348  use serde_json::json;
349
350  use super::*;
351  use crate::config::components::ManifestComponentBuilder;
352  use crate::config::{Codec, ComponentOperationExpressionBuilder, LiquidJsonConfig, MiddlewareBuilder};
353
354  #[test]
355  fn test_trigger_render() -> Result<()> {
356    let op = ComponentOperationExpressionBuilder::default()
357      .component(ComponentDefinition::Manifest(
358        ManifestComponentBuilder::default()
359          .reference("this/that:0.0.1")
360          .build()?,
361      ))
362      .config(LiquidJsonConfig::try_from(
363        json!({"op_config_field": "{{ctx.env.CARGO_MANIFEST_DIR}}"}),
364      )?)
365      .name("test")
366      .build()?;
367    let trigger = HttpTriggerConfigBuilder::default()
368      .resource("URL")
369      .routers(vec![HttpRouterConfig::RawRouter(RawRouterConfig {
370        path: "/".to_owned(),
371        middleware: Some(
372          MiddlewareBuilder::default()
373            .request(vec![op.clone()])
374            .response(vec![op.clone()])
375            .build()?,
376        ),
377        codec: Some(Codec::Json),
378        operation: op,
379      })])
380      .build()?;
381    let mut config = AppConfigurationBuilder::default()
382      .name("test")
383      .resources(vec![Binding::new("PORT", TcpPort::new("0.0.0.0", 90))])
384      .triggers(vec![TriggerDefinition::Http(trigger)])
385      .build()?;
386
387    config.env = Some(std::env::vars().collect());
388    config.root_config = Some(json!({"config_val": "from_config"}).try_into()?);
389
390    config.initialize()?;
391
392    let TriggerDefinition::Http(mut trigger) = config.triggers.pop().unwrap() else {
393      unreachable!();
394    };
395
396    let HttpRouterConfig::RawRouter(mut router) = trigger.routers.pop().unwrap() else {
397      unreachable!();
398    };
399
400    let cargo_manifest_dir = json!(env!("CARGO_MANIFEST_DIR"));
401
402    let config = router.operation.config.take().unwrap().value.unwrap();
403    assert_eq!(config.get("op_config_field"), Some(&cargo_manifest_dir));
404
405    let mut mw = router.middleware.take().unwrap();
406
407    let mw_req_config = mw.request[0].config.take().unwrap().value.unwrap();
408    assert_eq!(mw_req_config.get("op_config_field"), Some(&cargo_manifest_dir));
409
410    let mw_res_config = mw.response[0].config.take().unwrap().value.unwrap();
411    assert_eq!(mw_res_config.get("op_config_field"), Some(&cargo_manifest_dir));
412
413    Ok(())
414  }
415}