1use crate::model::{
16 ComposableAppGroupName, GuestLanguage, PackageName, TargetExistsResolveDecision,
17 TargetExistsResolveMode, Template, TemplateKind, TemplateMetadata, TemplateName,
18 TemplateParameters,
19};
20use anyhow::Context;
21use include_dir::{include_dir, Dir, DirEntry};
22use itertools::Itertools;
23use std::borrow::Cow;
24use std::collections::{BTreeMap, BTreeSet};
25use std::path::{Path, PathBuf};
26use std::{fs, io};
27
28pub mod model;
29
30#[cfg(test)]
31test_r::enable!();
32
33static TEMPLATES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
34static ADAPTERS: Dir<'_> = include_dir!("$OUT_DIR/golem-wit/adapters");
35static WIT: Dir<'_> = include_dir!("$OUT_DIR/golem-wit/wit/deps");
36
37fn all_templates() -> Vec<Template> {
38 let mut result: Vec<Template> = vec![];
39 for entry in TEMPLATES.entries() {
40 if let Some(lang_dir) = entry.as_dir() {
41 let lang_dir_name = lang_dir.path().file_name().unwrap().to_str().unwrap();
42 if let Some(lang) = GuestLanguage::from_string(lang_dir_name) {
43 let adapters_path =
44 Path::new(lang.tier().name()).join("wasi_snapshot_preview1.wasm");
45
46 for sub_entry in lang_dir.entries() {
47 if let Some(template_dir) = sub_entry.as_dir() {
48 let template_dir_name =
49 template_dir.path().file_name().unwrap().to_str().unwrap();
50 if template_dir_name != "INSTRUCTIONS"
51 && !template_dir_name.starts_with('.')
52 {
53 let template = parse_template(
54 lang,
55 lang_dir.path(),
56 Path::new("INSTRUCTIONS"),
57 &adapters_path,
58 template_dir.path(),
59 );
60
61 result.push(template);
62 }
63 }
64 }
65 } else {
66 panic!("Invalid guest language name: {lang_dir_name}");
67 }
68 }
69 }
70 result
71}
72
73pub fn all_standalone_templates() -> Vec<Template> {
74 all_templates()
75 .into_iter()
76 .filter(|template| matches!(template.kind, TemplateKind::Standalone))
77 .collect()
78}
79
80#[derive(Debug, Default)]
81pub struct ComposableAppTemplate {
82 pub common: Option<Template>,
83 pub components: BTreeMap<TemplateName, Template>,
84}
85
86pub fn all_composable_app_templates(
87) -> BTreeMap<GuestLanguage, BTreeMap<ComposableAppGroupName, ComposableAppTemplate>> {
88 let mut templates =
89 BTreeMap::<GuestLanguage, BTreeMap<ComposableAppGroupName, ComposableAppTemplate>>::new();
90
91 fn app_templates<'a>(
92 templates: &'a mut BTreeMap<
93 GuestLanguage,
94 BTreeMap<ComposableAppGroupName, ComposableAppTemplate>,
95 >,
96 language: GuestLanguage,
97 group: &ComposableAppGroupName,
98 ) -> &'a mut ComposableAppTemplate {
99 let groups = templates.entry(language).or_default();
100 if !groups.contains_key(group) {
101 groups.insert(group.clone(), ComposableAppTemplate::default());
102 }
103 groups.get_mut(group).unwrap()
104 }
105
106 for template in all_templates() {
107 match &template.kind {
108 TemplateKind::Standalone => continue,
109 TemplateKind::ComposableAppCommon { group, .. } => {
110 let common = &mut app_templates(&mut templates, template.language, group).common;
111 if let Some(common) = common {
112 panic!(
113 "Multiple common templates were found for {} - {}, template paths: {}, {}",
114 template.language,
115 group,
116 common.template_path.display(),
117 template.template_path.display()
118 );
119 }
120 *common = Some(template);
121 }
122 TemplateKind::ComposableAppComponent { group } => {
123 app_templates(&mut templates, template.language, group)
124 .components
125 .insert(template.name.clone(), template);
126 }
127 }
128 }
129
130 templates
131}
132
133pub fn instantiate_template(
134 template: &Template,
135 parameters: &TemplateParameters,
136 resolve_mode: TargetExistsResolveMode,
137) -> io::Result<String> {
138 instantiate_directory(
139 &TEMPLATES,
140 &template.template_path,
141 ¶meters.target_path,
142 template,
143 parameters,
144 resolve_mode,
145 )?;
146 if let Some(adapter_path) = &template.adapter_source {
147 let adapter_dir = {
148 parameters
149 .target_path
150 .join(match &template.adapter_target {
151 Some(target) => target.clone(),
152 None => PathBuf::from("adapters"),
153 })
154 .join(template.language.tier().name())
155 };
156
157 fs::create_dir_all(&adapter_dir)?;
158 copy(
159 &ADAPTERS,
160 adapter_path,
161 &adapter_dir.join(adapter_path.file_name().unwrap().to_str().unwrap()),
162 TargetExistsResolveMode::MergeOrSkip,
163 )?;
164 }
165 let wit_deps_targets = {
166 match &template.wit_deps_targets {
167 Some(paths) => paths
168 .iter()
169 .map(|path| parameters.target_path.join(path))
170 .collect(),
171 None => vec![parameters.target_path.join("wit").join("deps")],
172 }
173 };
174 for wit_dep in &template.wit_deps {
175 for target_wit_deps in &wit_deps_targets {
176 let target = target_wit_deps.join(wit_dep.file_name().unwrap().to_str().unwrap());
177 copy_all(&WIT, wit_dep, &target, TargetExistsResolveMode::MergeOrSkip)?;
178 }
179 }
180 Ok(render_template_instructions(template, parameters))
181}
182
183pub fn add_component_by_template(
184 common_template: Option<&Template>,
185 component_template: Option<&Template>,
186 target_path: &Path,
187 package_name: &PackageName,
188) -> anyhow::Result<()> {
189 let parameters = TemplateParameters {
190 component_name: package_name.to_string_with_colon().into(),
191 package_name: package_name.clone(),
192 target_path: target_path.into(),
193 };
194
195 if let Some(common_template) = common_template {
196 let skip = {
197 if let TemplateKind::ComposableAppCommon {
198 skip_if_exists: Some(file),
199 ..
200 } = &common_template.kind
201 {
202 target_path.join(file).exists()
203 } else {
204 false
205 }
206 };
207
208 if !skip {
209 instantiate_template(
210 common_template,
211 ¶meters,
212 TargetExistsResolveMode::MergeOrSkip,
213 )
214 .context(format!(
215 "Instantiating common template {}",
216 common_template.name
217 ))?;
218 }
219 }
220
221 if let Some(component_template) = component_template {
222 instantiate_template(
223 component_template,
224 ¶meters,
225 TargetExistsResolveMode::MergeOrFail,
226 )
227 .context(format!(
228 "Instantiating component template {}",
229 component_template.name
230 ))?;
231 }
232
233 Ok(())
234}
235
236pub fn render_template_instructions(
237 template: &Template,
238 parameters: &TemplateParameters,
239) -> String {
240 transform(&template.instructions, parameters)
241}
242
243fn instantiate_directory(
244 catalog: &Dir<'_>,
245 source: &Path,
246 target: &Path,
247 template: &Template,
248 parameters: &TemplateParameters,
249 resolve_mode: TargetExistsResolveMode,
250) -> io::Result<()> {
251 fs::create_dir_all(target)?;
252 for entry in catalog
253 .get_dir(source)
254 .unwrap_or_else(|| panic!("Could not find entry {source:?}"))
255 .entries()
256 {
257 let name = entry.path().file_name().unwrap().to_str().unwrap();
258 if !template.exclude.contains(name) && (name != "metadata.json") {
259 let name = file_name_transform(name, parameters);
260 match entry {
261 DirEntry::Dir(dir) => {
262 instantiate_directory(
263 catalog,
264 dir.path(),
265 &target.join(&name),
266 template,
267 parameters,
268 resolve_mode,
269 )?;
270 }
271 DirEntry::File(file) => {
272 instantiate_file(
273 catalog,
274 file.path(),
275 &target.join(&name),
276 parameters,
277 template.transform && !template.transform_exclude.contains(&name),
278 resolve_mode,
279 )?;
280 }
281 }
282 }
283 }
284 Ok(())
285}
286
287fn instantiate_file(
288 catalog: &Dir<'_>,
289 source: &Path,
290 target: &Path,
291 parameters: &TemplateParameters,
292 transform_contents: bool,
293 resolve_mode: TargetExistsResolveMode,
294) -> io::Result<()> {
295 match get_resolved_contents(catalog, source, target, resolve_mode)? {
296 Some(contents) => {
297 if transform_contents {
298 fs::write(
299 target,
300 transform(
301 std::str::from_utf8(contents.as_ref()).map_err(|err| {
302 io::Error::other(format!(
303 "Failed to decode as utf8, source: {}, err: {}",
304 source.display(),
305 err
306 ))
307 })?,
308 parameters,
309 ),
310 )
311 } else {
312 fs::write(target, contents)
313 }
314 }
315 None => Ok(()),
316 }
317}
318
319fn copy(
320 catalog: &Dir<'_>,
321 source: &Path,
322 target: &Path,
323 resolve_mode: TargetExistsResolveMode,
324) -> io::Result<()> {
325 match get_resolved_contents(catalog, source, target, resolve_mode)? {
326 Some(contents) => fs::write(target, contents),
327 None => Ok(()),
328 }
329}
330
331fn copy_all(
332 catalog: &Dir<'_>,
333 source_path: &Path,
334 target_path: &Path,
335 resolve_mode: TargetExistsResolveMode,
336) -> io::Result<()> {
337 let source_dir = catalog.get_dir(source_path).ok_or_else(|| {
338 io::Error::other(format!(
339 "Could not find dir {} in catalog",
340 source_path.display()
341 ))
342 })?;
343
344 fs::create_dir_all(target_path)?;
345
346 for file in source_dir.files() {
347 copy(
348 catalog,
349 file.path(),
350 &target_path.join(file.path().file_name().unwrap().to_str().unwrap()),
351 resolve_mode,
352 )?;
353 }
354
355 Ok(())
356}
357
358fn transform(str: impl AsRef<str>, parameters: &TemplateParameters) -> String {
359 str.as_ref()
360 .replace("componentname", parameters.component_name.as_str())
361 .replace("component-name", ¶meters.component_name.to_kebab_case())
362 .replace("ComponentName", ¶meters.component_name.to_pascal_case())
363 .replace("componentName", ¶meters.component_name.to_camel_case())
364 .replace("component_name", ¶meters.component_name.to_snake_case())
365 .replace(
366 "pack::name",
367 ¶meters.package_name.to_string_with_double_colon(),
368 )
369 .replace("pa_ck::na_me", ¶meters.package_name.to_rust_binding())
370 .replace("pack:name", ¶meters.package_name.to_string_with_colon())
371 .replace("pack_name", ¶meters.package_name.to_snake_case())
372 .replace("pack-name", ¶meters.package_name.to_kebab_case())
373 .replace("pack/name", ¶meters.package_name.to_string_with_slash())
374 .replace("PackName", ¶meters.package_name.to_pascal_case())
375 .replace("pack-ns", ¶meters.package_name.namespace())
376 .replace("PackNs", ¶meters.package_name.namespace_title_case())
377 .replace("__pack__", ¶meters.package_name.namespace_snake_case())
378 .replace("__name__", ¶meters.package_name.name_snake_case())
379}
380
381fn file_name_transform(str: impl AsRef<str>, parameters: &TemplateParameters) -> String {
382 transform(str, parameters).replace("Cargo.toml._", "Cargo.toml") }
384
385fn check_target(
386 target: &Path,
387 resolve_mode: TargetExistsResolveMode,
388) -> io::Result<Option<TargetExistsResolveDecision>> {
389 if !target.exists() {
390 return Ok(None);
391 }
392
393 let get_merge = || -> io::Result<Option<TargetExistsResolveDecision>> {
394 let file_name = target
395 .file_name()
396 .ok_or_else(|| {
397 io::Error::other(format!(
398 "Failed to get file name for target: {}",
399 target.display()
400 ))
401 })
402 .and_then(|file_name| {
403 file_name.to_str().ok_or_else(|| {
404 io::Error::other(format!(
405 "Failed to convert file name to string: {}",
406 file_name.to_string_lossy()
407 ))
408 })
409 })?;
410
411 match file_name {
412 ".gitignore" => {
413 let target = target.to_path_buf();
414 let current_content = fs::read_to_string(&target)?;
415 Ok(Some(TargetExistsResolveDecision::Merge(Box::new(
416 move |new_content: &[u8]| -> io::Result<Vec<u8>> {
417 Ok(current_content
418 .lines()
419 .chain(
420 std::str::from_utf8(new_content).map_err(|err| {
421 io::Error::other(format!(
422 "Failed to decode new content for merge as utf8, target: {}, err: {}",
423 target.display(),
424 err
425 ))
426 })?.lines(),
427 )
428 .collect::<BTreeSet<&str>>()
429 .iter()
430 .join("\n")
431 .into_bytes())
432 },
433 ))))
434 }
435 _ => Ok(None),
436 }
437 };
438
439 let target_already_exists = || {
440 Err(io::Error::other(format!(
441 "Target ({}) already exists!",
442 target.display()
443 )))
444 };
445
446 match resolve_mode {
447 TargetExistsResolveMode::Skip => Ok(Some(TargetExistsResolveDecision::Skip)),
448 TargetExistsResolveMode::MergeOrSkip => match get_merge()? {
449 Some(merge) => Ok(Some(merge)),
450 None => Ok(Some(TargetExistsResolveDecision::Skip)),
451 },
452 TargetExistsResolveMode::Fail => target_already_exists(),
453 TargetExistsResolveMode::MergeOrFail => match get_merge()? {
454 Some(merge) => Ok(Some(merge)),
455 None => target_already_exists(),
456 },
457 }
458}
459
460fn get_contents<'a>(catalog: &Dir<'a>, source: &'a Path) -> io::Result<&'a [u8]> {
461 Ok(catalog
462 .get_file(source)
463 .ok_or_else(|| io::Error::other(format!("Could not find entry {}", source.display())))?
464 .contents())
465}
466
467fn get_resolved_contents<'a>(
468 catalog: &Dir<'a>,
469 source: &'a Path,
470 target: &'a Path,
471 resolve_mode: TargetExistsResolveMode,
472) -> io::Result<Option<Cow<'a, [u8]>>> {
473 match check_target(target, resolve_mode)? {
474 None => Ok(Some(Cow::Borrowed(get_contents(catalog, source)?))),
475 Some(TargetExistsResolveDecision::Skip) => Ok(None),
476 Some(TargetExistsResolveDecision::Merge(merge)) => {
477 Ok(Some(Cow::Owned(merge(get_contents(catalog, source)?)?)))
478 }
479 }
480}
481
482fn parse_template(
483 lang: GuestLanguage,
484 lang_path: &Path,
485 default_instructions_file_name: &Path,
486 adapters_path: &Path,
487 template_root: &Path,
488) -> Template {
489 let raw_metadata = TEMPLATES
490 .get_file(template_root.join("metadata.json"))
491 .expect("Failed to read metadata JSON")
492 .contents();
493 let metadata = serde_json::from_slice::<TemplateMetadata>(raw_metadata)
494 .expect("Failed to parse metadata JSON");
495
496 let kind = match (metadata.app_common_group, metadata.app_component_group) {
497 (None, None) => TemplateKind::Standalone,
498 (Some(group), None) => TemplateKind::ComposableAppCommon {
499 group: group.into(),
500 skip_if_exists: metadata.app_common_skip_if_exists.map(PathBuf::from),
501 },
502 (None, Some(group)) => TemplateKind::ComposableAppComponent {
503 group: group.into(),
504 },
505 (Some(_), Some(_)) => panic!(
506 "Only one of appCommonGroup and appComponentGroup can be specified, template root: {}",
507 template_root.display()
508 ),
509 };
510
511 let instructions = match &kind {
512 TemplateKind::Standalone => {
513 let instructions_path = match metadata.instructions {
514 Some(instructions_file_name) => lang_path.join(instructions_file_name),
515 None => lang_path.join(default_instructions_file_name),
516 };
517
518 let raw_instructions = TEMPLATES
519 .get_file(instructions_path)
520 .expect("Failed to read instructions")
521 .contents();
522
523 String::from_utf8(raw_instructions.to_vec()).expect("Failed to decode instructions")
524 }
525 TemplateKind::ComposableAppCommon { .. } => "".to_string(),
526 TemplateKind::ComposableAppComponent { .. } => "".to_string(),
527 };
528
529 let name: TemplateName = {
530 let name = template_root
531 .file_name()
532 .unwrap()
533 .to_str()
534 .unwrap()
535 .to_string();
536
537 let segments = name.split("-").collect::<Vec<_>>();
540 if segments.len() > 2 && segments[1] == "app" {
541 if segments.len() > 3 && segments[2] == "component" {
542 segments[3..].join("-").into()
543 } else {
544 segments[2..].join("-").into()
545 }
546 } else {
547 name.into()
548 }
549 };
550
551 let mut wit_deps: Vec<PathBuf> = vec![];
552 if metadata.requires_golem_host_wit.unwrap_or(false) {
553 WIT.dirs()
554 .filter(|&dir| dir.path().starts_with("golem"))
555 .map(|dir| dir.path())
556 .for_each(|path| {
557 wit_deps.push(path.to_path_buf());
558 });
559
560 wit_deps.push(PathBuf::from("golem-1.x"));
561 wit_deps.push(PathBuf::from("wasm-rpc"));
562 }
563 if metadata.requires_wasi.unwrap_or(false) {
564 wit_deps.push(PathBuf::from("blobstore"));
565 wit_deps.push(PathBuf::from("cli"));
566 wit_deps.push(PathBuf::from("clocks"));
567 wit_deps.push(PathBuf::from("filesystem"));
568 wit_deps.push(PathBuf::from("http"));
569 wit_deps.push(PathBuf::from("io"));
570 wit_deps.push(PathBuf::from("keyvalue"));
571 wit_deps.push(PathBuf::from("logging"));
572 wit_deps.push(PathBuf::from("random"));
573 wit_deps.push(PathBuf::from("sockets"));
574 }
575
576 let requires_adapter = metadata
577 .requires_adapter
578 .unwrap_or(metadata.adapter_target.is_some());
579
580 Template {
581 name,
582 kind,
583 language: lang,
584 description: metadata.description,
585 template_path: template_root.to_path_buf(),
586 instructions,
587 adapter_source: {
588 if requires_adapter {
589 Some(adapters_path.to_path_buf())
590 } else {
591 None
592 }
593 },
594 adapter_target: metadata.adapter_target.map(PathBuf::from),
595 wit_deps,
596 wit_deps_targets: metadata
597 .wit_deps_paths
598 .map(|dirs| dirs.iter().map(PathBuf::from).collect()),
599 exclude: metadata
600 .exclude
601 .unwrap_or_default()
602 .iter()
603 .cloned()
604 .collect(),
605 transform_exclude: metadata
606 .transform_exclude
607 .map(|te| te.iter().cloned().collect())
608 .unwrap_or_default(),
609 transform: metadata.transform.unwrap_or(true),
610 }
611}