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