golem_examples/
lib.rs

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        &parameters.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                &parameters,
194                TargetExistsResolveMode::MergeOrSkip,
195            )?;
196        }
197    }
198
199    instantiate_example(
200        component_example,
201        &parameters,
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", &parameters.component_name.to_kebab_case())
331        .replace("ComponentName", &parameters.component_name.to_pascal_case())
332        .replace("componentName", &parameters.component_name.to_camel_case())
333        .replace("component_name", &parameters.component_name.to_snake_case())
334        .replace(
335            "pack::name",
336            &parameters.package_name.to_string_with_double_colon(),
337        )
338        .replace("pa_ck::na_me", &parameters.package_name.to_rust_binding())
339        .replace("pack:name", &parameters.package_name.to_string_with_colon())
340        .replace("pack_name", &parameters.package_name.to_snake_case())
341        .replace("pack-name", &parameters.package_name.to_kebab_case())
342        .replace("pack/name", &parameters.package_name.to_string_with_slash())
343        .replace("PackName", &parameters.package_name.to_pascal_case())
344        .replace("pack-ns", &parameters.package_name.namespace())
345        .replace("PackNs", &parameters.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") // HACK because cargo package ignores every subdirectory containing a Cargo.toml
350}
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}