1#![allow(missing_docs)] use 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]
41pub struct AppConfiguration {
46 #[asset(skip)]
47 pub(crate) name: String,
49
50 #[asset(skip)]
51 #[builder(setter(strip_option), default)]
52 #[property(skip)]
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub(crate) source: Option<PathBuf>,
56
57 #[builder(setter(strip_option), default)]
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub(crate) metadata: Option<config::Metadata>,
61
62 #[builder(setter(strip_option), default)]
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub(crate) package: Option<PackageConfig>,
66
67 #[builder(default)]
68 #[serde(skip_serializing_if = "Vec::is_empty")]
70 pub(crate) import: Vec<Binding<ImportDefinition>>,
71
72 #[builder(default)]
73 #[serde(skip_serializing_if = "Vec::is_empty")]
75 pub(crate) resources: Vec<Binding<ResourceDefinition>>,
76
77 #[builder(default)]
78 #[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 #[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 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 pub fn package_files(&self) -> Assets<AssetReference> {
131 self.package.assets()
132 }
133
134 #[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 pub fn resolve_binding(&self, name: &BoundIdentifier) -> Result<OwnedConfigurationItem> {
147 resolve(name, &self.import, &self.resources)
148 }
149
150 #[must_use]
152 pub fn resolver(&self) -> Box<Resolver> {
153 make_resolver(self.import.clone(), self.resources.clone())
154 }
155
156 #[must_use]
158 pub fn source(&self) -> Option<&Path> {
159 self.source.as_deref()
160 }
161
162 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 if !source.is_dir() {
173 source.pop();
174 }
175 self.set_baseurl(&source);
176 }
177
178 #[must_use]
180 pub fn version(&self) -> Option<&str> {
181 self.metadata.as_ref().map(|m| m.version.as_str())
182 }
183
184 pub fn add_resource<T: Into<String>>(&mut self, name: T, resource: ResourceDefinition) {
186 self.resources.push(Binding::new(name, resource));
187 }
188
189 #[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 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 pub const fn validate(&self) -> Result<()> {
225 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#[derive(Debug, Clone, PartialEq)]
268#[must_use]
269
270pub enum ConfigurationItem<'a> {
271 Component(&'a ComponentDefinition),
273 Types(&'a TypesComponent),
275 Resource(&'a ResourceDefinition),
277}
278
279impl<'a> ConfigurationItem<'a> {
280 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 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 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#[derive(Debug, Clone, PartialEq)]
307#[must_use]
308
309pub enum OwnedConfigurationItem {
310 Component(ComponentDefinition),
312 Resource(ResourceDefinition),
314}
315
316impl OwnedConfigurationItem {
317 #[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 #[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 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}