1use crate::wit::from_wit_package_name_to_pkg_fqn;
2use anyhow::Context;
3use concepts::{
4 ComponentType, ContentDigest, FnName, FunctionExtension, FunctionFqn, FunctionMetadata,
5 IfcFqnName, PackageIfcFns, ParameterType, ParameterTypes, PkgFqn, ReturnType,
6 ReturnTypeExtendable, ReturnTypeNonExtendable, SUFFIX_FN_AWAIT_NEXT, SUFFIX_FN_CANCELLABLE,
7 SUFFIX_FN_GET, SUFFIX_FN_SCHEDULE, SUFFIX_FN_STUB, SUFFIX_FN_SUBMIT, SUFFIX_PKG_EXT,
8 SUFFIX_PKG_SCHEDULE, SUFFIX_PKG_STUB, StrVariant,
9};
10use indexmap::{IndexMap, indexmap};
11use std::{
12 fmt::Display,
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16use strum::IntoEnumIterator as _;
17use tracing::{debug, error, info, instrument, trace, warn};
18use tracing_error::SpanTrace;
19use val_json::type_wrapper::{TypeKey, TypeWrapper};
20use wit_component::{ComponentEncoder, WitPrinter};
21use wit_parser::{InterfaceId, PackageId, Param, Resolve, World, WorldKey, decoding::DecodedWasm};
22
23#[derive(derive_more::Debug, Clone)]
24pub struct WasmComponent {
25 pub exim: ExIm,
26 resolve: Resolve, main_pkg_id: PackageId,
28}
29
30impl WasmComponent {
31 pub async fn convert_core_module_to_component(
37 wasm_path: &Path,
38 input_content_digest: &ContentDigest,
39 output_parent: &Path,
40 ) -> Result<Option<PathBuf>, anyhow::Error> {
41 use tokio::io::AsyncReadExt;
42
43 let stopwatch = std::time::Instant::now();
44 let mut wasm_file = tokio::fs::File::open(wasm_path)
45 .await
46 .with_context(|| format!("cannot open {wasm_path:?}"))?;
47
48 let mut header_vec = Vec::new();
49 loop {
50 let mut header = [0_u8; 8];
51 let n = wasm_file.read(&mut header).await?;
52 if n == 0 {
53 break;
54 }
55 header_vec.extend(header[..n].iter());
56 if header_vec.len() >= 8 {
57 break;
59 }
60 }
61 if wasmparser::Parser::is_component(&header_vec) {
62 return Ok(None);
63 }
64 if !wasmparser::Parser::is_core_wasm(&header_vec) {
65 anyhow::bail!("not a WASM Component or a Core WASM Module: {wasm_file:?}");
66 }
67
68 let output_file = output_parent.join(format!(
69 "{}_transformed.wasm",
70 input_content_digest.with_infix("_"),
71 ));
72 if output_file.exists() {
74 debug!("Found the transformed WASM Component {output_file:?}");
75 return Ok(Some(output_file));
76 }
77
78 let wasm = tokio::fs::read(wasm_path).await?;
79 let mut encoder = ComponentEncoder::default().validate(true);
80 encoder = encoder.module(&wasm)?;
81 let component_contents = encoder
82 .encode()
83 .with_context(|| {
84 format!(
85 "failed to transform a WASM Component from the Core WASM Module {wasm_path:?}"
86 )
87 })
88 .inspect_err(|err| error!("{err:?}"))?;
89
90 tokio::fs::write(&output_file, component_contents)
91 .await
92 .with_context(|| {
93 format!("cannot write the transformed WASM Component to {output_file:?}")
94 })
95 .inspect_err(|err| error!("{err:?}"))?;
96 info!(
97 "Transformed Core WASM Module {wasm_path:?} to WASM Component {output_file:?} in {:?}",
98 stopwatch.elapsed()
99 );
100 Ok(Some(output_file))
101 }
102
103 pub fn verify_wasm(wasm_path: impl AsRef<Path>) -> Result<(), DecodeError> {
106 let wasm_path = wasm_path.as_ref();
107 Self::decode_using_wit_parser_inner(
108 wasm_path, false, )?;
110 Ok(())
111 }
112
113 pub fn new(
114 wasm_path: impl AsRef<Path>,
115 component_type: ComponentType,
116 ) -> Result<Self, DecodeError> {
117 let wasm_path = wasm_path.as_ref();
118 let (exim_lite, resolve, main_pkg_id) =
119 Self::decode_using_wit_parser(wasm_path, component_type)?;
120
121 let exim = ExIm::decode(exim_lite, component_type)?;
122 let (resolve, main_pkg_id) = crate::wit::rebuild_resolve(&exim, resolve, main_pkg_id)
123 .map_err(|err| {
124 DecodeError::new_with_source(
125 format!("cannot rebuild resolve from {wasm_path:?}"),
126 err,
127 )
128 })?;
129 Ok(Self {
130 exim,
131 resolve,
132 main_pkg_id,
133 })
134 }
135
136 pub fn new_from_wit_string(
139 wit: &str,
140 component_type: ComponentType,
141 ) -> Result<Self, DecodeError> {
142 let mut resolve = Resolve::default();
143 let group =
144 wit_parser::UnresolvedPackageGroup::parse(PathBuf::new(), wit).map_err(|source| {
145 DecodeError::new_with_source(format!("cannot parse synthesized WIT: {wit}"), source)
146 })?;
147 let main_pkg_id = resolve.push_group(group).map_err(|source| {
148 DecodeError::new_with_source(
149 format!("cannot push synthesized WIT group: {wit}"),
150 source,
151 )
152 })?;
153 let world_id = resolve
154 .select_world(&[main_pkg_id], None)
155 .map_err(|source| {
156 DecodeError::new_with_source(
157 "cannot select default world from synthesized WIT".to_string(),
158 source,
159 )
160 })?;
161 let world = resolve
162 .worlds
163 .get(world_id)
164 .expect("world was found by wit-parser");
165 let exim_lite =
166 Self::create_exim_lite(&resolve, world, has_submittable_exports(component_type))?;
167
168 let exim = ExIm::decode(exim_lite, component_type)?;
169 let (resolve, main_pkg_id) = crate::wit::rebuild_resolve(&exim, resolve, main_pkg_id)
170 .map_err(|err| {
171 DecodeError::new_with_source(
172 "cannot rebuild resolve from synthesized WIT".to_string(),
173 err,
174 )
175 })?;
176 Ok(Self {
177 exim,
178 resolve,
179 main_pkg_id,
180 })
181 }
182
183 pub fn new_from_fn_signature(
187 ffqn: &FunctionFqn,
188 params: &[ParameterType],
189 return_type: &ReturnTypeExtendable,
190 component_type: ComponentType,
191 world_name: &str,
192 ) -> Result<Self, DecodeError> {
193 let fn_metadata = FunctionMetadata {
194 ffqn: ffqn.clone(),
195 parameter_types: ParameterTypes(params.to_vec()),
196 return_type: ReturnType::Extendable(return_type.clone()),
197 extension: None,
198 submittable: has_submittable_exports(component_type),
199 };
200 let pkg_ifc_fns = PackageIfcFns {
201 ifc_fqn: ffqn.ifc_fqn.clone(),
202 extension: false,
203 fns: IndexMap::from_iter([(ffqn.function_name.clone(), fn_metadata)]),
204 };
205 let exim_lite = ExImLite {
206 exports: vec![pkg_ifc_fns.clone()],
207 imports: vec![],
208 };
209 let exim = ExIm::decode(exim_lite, component_type)?;
210 let pkg_fqn = ffqn.ifc_fqn.pkg_fqn_name();
211 let (resolve, main_pkg_id) =
212 crate::wit::build_primary_resolve(&pkg_fqn, &[&pkg_ifc_fns], Some(world_name))
213 .map_err(|e| {
214 DecodeError::new_with_source("cannot build resolve for JS function", e)
215 })?;
216 let (resolve, main_pkg_id) = crate::wit::rebuild_resolve(&exim, resolve, main_pkg_id)
217 .map_err(|e| {
218 DecodeError::new_with_source("cannot rebuild resolve for JS function", e)
219 })?;
220 Ok(Self {
221 exim,
222 resolve,
223 main_pkg_id,
224 })
225 }
226
227 pub fn new_from_wit_folder(
228 path: impl AsRef<Path>,
229 component_type: ComponentType,
230 ) -> Result<Self, DecodeError> {
231 let path = path.as_ref();
232 let mut resolve = Resolve::default();
233 let (main_pkg_id, _) = resolve.push_dir(path).map_err(|source| {
234 DecodeError::new_with_source(
235 format!("cannot parse the WIT directory of {path:?}"),
236 source,
237 )
238 })?;
239 let world_id = resolve
240 .select_world(&[main_pkg_id], None)
241 .map_err(|source| {
242 DecodeError::new_with_source(
243 format!("cannot select the default world of {path:?}"),
244 source,
245 )
246 })?;
247 let world = resolve
248 .worlds
249 .get(world_id)
250 .expect("world was found by wit-parser");
251 let exim_lite =
252 Self::create_exim_lite(&resolve, world, has_submittable_exports(component_type))?;
253
254 let exim = ExIm::decode(exim_lite, component_type)?;
255 let (resolve, main_pkg_id) = crate::wit::rebuild_resolve(&exim, resolve, main_pkg_id)
256 .map_err(|err| {
257 DecodeError::new_with_source(format!("cannot rebuild resolve from {path:?}"), err)
258 })?;
259 Ok(Self {
260 exim,
261 resolve,
262 main_pkg_id,
263 })
264 }
265
266 #[must_use]
267 pub fn exported_functions(&self, extensions: bool) -> &[FunctionMetadata] {
268 if extensions {
269 &self.exim.exports_flat_ext
270 } else {
271 &self.exim.exports_flat_noext
272 }
273 }
274
275 #[must_use]
276 pub fn imported_functions(&self) -> &[FunctionMetadata] {
277 &self.exim.imports_flat
278 }
279
280 #[must_use]
285 pub fn wit(&self) -> String {
286 crate::wit::wit(&self.resolve, self.main_pkg_id)
287 .expect("WitPrinter on a post-rebuild_resolve Resolve cannot fail")
288 }
289
290 #[must_use]
291 pub fn exports_hierarchy_ext(&self) -> &[PackageIfcFns] {
292 self.exim.get_exports_hierarchy_ext()
293 }
294
295 pub fn exported_extension_wits(
296 &self,
297 ) -> Result<hashbrown::HashMap<PkgFqn, String>, anyhow::Error> {
298 let mut pkgs_to_wits = hashbrown::HashMap::new();
299
300 let exported_ext_packages: hashbrown::HashSet<_> = self
301 .exim
302 .exports_hierarchy_ext
303 .iter()
304 .filter_map(|pkg_ifc_fns| {
305 if pkg_ifc_fns.extension {
306 Some(pkg_ifc_fns.ifc_fqn.pkg_fqn_name().to_string())
307 } else {
308 None
309 }
310 })
311 .collect();
312
313 for (package_id, package) in self
314 .resolve
315 .packages
316 .iter()
317 .filter(|(_, package)| exported_ext_packages.contains(&package.name.to_string()))
318 {
319 let pkg_fqn = from_wit_package_name_to_pkg_fqn(&package.name);
320
321 let mut printer = wit_component::WitPrinter::default();
322 printer.print(&self.resolve, package_id, &[]).unwrap();
323 let wit = printer.output.to_string();
324
325 pkgs_to_wits.insert(pkg_fqn, wit);
326 }
327 Ok(pkgs_to_wits)
328 }
329
330 fn decode_using_wit_parser(
331 wasm_path: &Path,
332 component_type: ComponentType,
333 ) -> Result<(ExImLite, Resolve, PackageId), DecodeError> {
334 Self::decode_using_wit_parser_inner(wasm_path, has_submittable_exports(component_type))
335 }
336
337 #[instrument]
338 fn decode_using_wit_parser_inner(
339 wasm_path: &Path,
340 submittable_exports: bool, ) -> Result<(ExImLite, Resolve, PackageId), DecodeError> {
342 trace!("Decoding using wit_parser");
343
344 let wasm_file = std::fs::File::open(wasm_path)
345 .with_context(|| format!("cannot open {wasm_path:?}"))
346 .map_err(|err| {
347 DecodeError::new_with_source(format!("cannot read {wasm_path:?}"), err)
348 })?;
349 let stopwatch = std::time::Instant::now();
350 let decoded = wit_parser::decoding::decode_reader(wasm_file).map_err(|err| {
351 DecodeError::new_with_source(format!("cannot read {wasm_path:?} using wit_parser"), err)
352 })?;
353 let main_package = decoded.package();
354 let DecodedWasm::Component(resolve, world_id) = decoded else {
355 return Err(DecodeError::new_without_source(format!(
356 "{wasm_path:?} must be a WASM Component"
357 )));
358 };
359 let world = resolve
360 .worlds
361 .get(world_id)
362 .expect("world was found by wit-parser");
363 let exim_lite = Self::create_exim_lite(&resolve, world, submittable_exports)?;
364 debug!("Parsed with wit_parser in {:?}", stopwatch.elapsed());
365 trace!("{exim_lite:?}");
366 Ok((exim_lite, resolve, main_package))
367 }
368
369 fn create_exim_lite(
370 resolve: &Resolve,
371 world: &World,
372 submittable_exports: bool,
373 ) -> Result<ExImLite, DecodeError> {
374 let exports = populate_ifcs_with_compatible_fns(
375 resolve,
376 world_interfaces(world, ExOrIm::Exports),
377 if submittable_exports {
378 ProcessingKind::ExportsSubmittable
379 } else {
380 ProcessingKind::ExportsOfActivityStub
381 },
382 )?;
383 let imports = populate_ifcs_with_compatible_fns(
384 resolve,
385 world_interfaces(world, ExOrIm::Imports),
386 ProcessingKind::Imports,
387 )?;
388 Ok(ExImLite { imports, exports })
389 }
390}
391
392#[derive(Clone, Copy, PartialEq)]
393pub(crate) enum ExOrIm {
394 Exports,
395 Imports,
396}
397
398fn world_interfaces(world: &World, exorim: ExOrIm) -> impl Iterator<Item = InterfaceId> {
399 match exorim {
400 ExOrIm::Exports => &world.exports,
401 ExOrIm::Imports => &world.imports,
402 }
403 .keys()
404 .filter_map(|world_key| {
405 if let WorldKey::Interface(ifc_id) = world_key {
406 Some(*ifc_id)
407 } else {
408 None
409 }
410 })
411}
412
413fn has_submittable_exports(component_type: ComponentType) -> bool {
414 !matches!(component_type, ComponentType::ActivityStub)
416}
417
418#[derive(Debug, thiserror::Error)]
419pub struct DecodeError {
420 reason: StrVariant,
421 #[source]
422 source: Option<Box<dyn std::error::Error + Send + Sync>>,
423 #[allow(dead_code)] context: SpanTrace,
425}
426impl DecodeError {
427 pub fn new_with_source(
428 reason: impl Into<StrVariant>,
429 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
430 ) -> Self {
431 DecodeError {
432 reason: reason.into(),
433 source: Some(source.into()),
434 context: SpanTrace::capture(),
435 }
436 }
437
438 pub fn new_without_source(reason: impl Into<StrVariant>) -> Self {
439 DecodeError {
440 reason: reason.into(),
441 source: None,
442 context: SpanTrace::capture(),
443 }
444 }
445}
446impl Display for DecodeError {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 write!(f, "{}", self.reason)?;
449 if let Some(source) = &self.source {
450 write!(f, " - {source}")?;
451 }
452 Ok(())
453 }
454}
455
456#[derive(Debug, Clone)]
457pub struct ExIm {
458 inner: Arc<ExImInner>,
459}
460
461impl std::ops::Deref for ExIm {
462 type Target = ExImInner;
463
464 fn deref(&self) -> &Self::Target {
465 &self.inner
466 }
467}
468
469#[derive(Debug)]
470pub struct ExImInner {
471 pub(crate) exports_hierarchy_ext: Vec<PackageIfcFns>,
472 exports_flat_noext: Vec<FunctionMetadata>,
473 exports_flat_ext: Vec<FunctionMetadata>,
474 pub imports_flat: Vec<FunctionMetadata>,
475}
476
477impl ExIm {
478 #[must_use]
479 pub fn get_exports(&self, extensions: bool) -> &[FunctionMetadata] {
480 if extensions {
481 &self.exports_flat_ext
482 } else {
483 &self.exports_flat_noext
484 }
485 }
486
487 #[must_use]
488 pub fn get_exports_hierarchy_ext(&self) -> &[PackageIfcFns] {
489 &self.exports_hierarchy_ext
490 }
491
492 fn decode(exim_lite: ExImLite, component_type: ComponentType) -> Result<ExIm, DecodeError> {
493 let mut exports_hierarchy_ext = exim_lite.exports;
494 for PackageIfcFns {
497 ifc_fqn,
498 fns,
499 extension,
500 } in &exports_hierarchy_ext
501 {
502 assert!(
503 !extension,
504 "the list must not be enriched with extensions yet"
505 );
506 if ifc_fqn.is_extension() {
507 return Err(DecodeError::new_without_source(format!(
508 "invalid package `{}`, {SUFFIX_PKG_EXT} is reserved",
509 ifc_fqn.package_name()
510 )));
511 }
512 if component_type != ComponentType::Workflow {
513 for fn_name in fns.keys() {
514 if fn_name.ends_with(SUFFIX_FN_CANCELLABLE) {
515 return Err(DecodeError::new_without_source(format!(
516 "invalid function `{ifc_fqn}.{fn_name}`, {SUFFIX_FN_CANCELLABLE} is reserved for workflow exports"
517 )));
518 }
519 }
520 }
521 }
522
523 let exports_flat_noext = Self::flatten(&exports_hierarchy_ext);
524 Self::enrich_exports_with_extensions(&mut exports_hierarchy_ext, component_type);
525 let exports_flat_ext = Self::flatten(&exports_hierarchy_ext);
526 let imports_hierarchy = exim_lite.imports;
527 let imports_flat = Self::flatten(&imports_hierarchy);
528 Ok(Self {
529 inner: Arc::new(ExImInner {
530 exports_hierarchy_ext,
531 exports_flat_noext,
532 exports_flat_ext,
533 imports_flat,
534 }),
535 })
536 }
537
538 fn enrich_exports_with_extensions(
539 exports_hierarchy: &mut Vec<PackageIfcFns>,
540 component_type: ComponentType,
541 ) {
542 if component_type == ComponentType::WebhookEndpoint {
543 return;
544 }
545 let execution_id_type_wrapper =
547 TypeWrapper::Record(indexmap! {TypeKey::new_kebab("id") => TypeWrapper::String});
548 let delay_id_type_wrapper =
549 TypeWrapper::Record(indexmap! {TypeKey::new_kebab("id") => TypeWrapper::String});
550 let join_set_id_type_wrapper = TypeWrapper::Borrow;
551
552 let return_type_execution_id = ReturnType::NonExtendable(ReturnTypeNonExtendable {
553 type_wrapper: execution_id_type_wrapper.clone(),
554 wit_type: concepts::StrVariant::Static("execution-id"),
555 });
556 let param_type_execution_id = ParameterType {
557 type_wrapper: execution_id_type_wrapper.clone(),
558 name: StrVariant::Static("execution-id"),
559 wit_type: StrVariant::Static("execution-id"),
560 };
561 let param_type_join_set = ParameterType {
562 type_wrapper: join_set_id_type_wrapper.clone(),
563 name: StrVariant::Static("join-set"),
564 wit_type: StrVariant::Static("borrow<join-set>"),
565 };
566 let duration_type_wrapper = TypeWrapper::Variant(indexmap! {
567 TypeKey::new_kebab("milliseconds") => Some(TypeWrapper::U64),
568 TypeKey::new_kebab("seconds") => Some(TypeWrapper::U64),
569 TypeKey::new_kebab("minutes") => Some(TypeWrapper::U32),
570 TypeKey::new_kebab("hours") => Some(TypeWrapper::U32),
571 TypeKey::new_kebab("days") => Some(TypeWrapper::U32),
572 });
573 let param_type_scheduled_at = ParameterType {
574 type_wrapper: TypeWrapper::Variant(indexmap! {
575 TypeKey::new_kebab("now") => None,
576 TypeKey::new_kebab("at") => Some(TypeWrapper::Record(indexmap! {
577 TypeKey::new_kebab("seconds") => TypeWrapper::U64,
578 TypeKey::new_kebab("nanoseconds") => TypeWrapper::U32,
579 })),
580 TypeKey::new_kebab("in") => Some(duration_type_wrapper),
581 }),
582 name: StrVariant::Static("scheduled-at"),
583 wit_type: StrVariant::Static("schedule-at"),
584 };
585
586 let function_type_wrapper = TypeWrapper::Record(indexmap! {
587 TypeKey::new_kebab("interface-name") => TypeWrapper::String,
588 TypeKey::new_kebab("function-name") => TypeWrapper::String,
589 });
590
591 let response_id = TypeWrapper::Variant(indexmap! {
592 TypeKey::new_kebab("execution-id") => Some(execution_id_type_wrapper.clone()),
593 TypeKey::new_kebab("delay-id") => Some(delay_id_type_wrapper.clone()),
594 });
595
596 let function_mismatch_type_wrapper = TypeWrapper::Record(indexmap! {
598 TypeKey::new_kebab("specified-function") => function_type_wrapper.clone(),
599 TypeKey::new_kebab("actual-function") => TypeWrapper::Option(Box::from(function_type_wrapper)),
600 TypeKey::new_kebab("actual-id") => response_id,
601 });
602
603 let await_next_extension_error_type_wrapper = TypeWrapper::Variant(indexmap! {
605 TypeKey::new_kebab("all-processed") => None,
606 TypeKey::new_kebab("function-mismatch") => Some(function_mismatch_type_wrapper.clone()),
607 });
608
609 let get_extension_error_type_wrapper = TypeWrapper::Variant(indexmap! {
611 TypeKey::new_kebab("function-mismatch") => Some(function_mismatch_type_wrapper.clone()),
612 TypeKey::new_kebab("not-found-in-processed-responses") => None,
613 });
614
615 let stub_error_type_wrapper = TypeWrapper::Variant(indexmap! {
617 TypeKey::new_kebab("execution-not-found") => None,
618 TypeKey::new_kebab("type-check-error") => Some(TypeWrapper::String),
619 TypeKey::new_kebab("conflict") => None,
620 });
621
622 let mut extensions = Vec::new();
623 let mut schedules = Vec::new();
624 let mut stubs = Vec::new();
625 for PackageIfcFns {
626 ifc_fqn,
627 fns,
628 extension,
629 } in exports_hierarchy.iter()
630 {
631 assert!(!extension);
632 let obelisk_ext_ifc = IfcFqnName::from_parts(
633 ifc_fqn.namespace(),
634 &format!("{}{SUFFIX_PKG_EXT}", ifc_fqn.package_name()),
635 ifc_fqn.ifc_name(),
636 ifc_fqn.version(),
637 );
638 let obelisk_schedule_ifc = IfcFqnName::from_parts(
639 ifc_fqn.namespace(),
640 &format!("{}{SUFFIX_PKG_SCHEDULE}", ifc_fqn.package_name()),
641 ifc_fqn.ifc_name(),
642 ifc_fqn.version(),
643 );
644 let obelisk_stub_ifc = IfcFqnName::from_parts(
645 ifc_fqn.namespace(),
646 &format!("{}{SUFFIX_PKG_STUB}", ifc_fqn.package_name()),
647 ifc_fqn.ifc_name(),
648 ifc_fqn.version(),
649 );
650
651 let mut extension_fns = IndexMap::new();
652 let mut schedule_fns = IndexMap::new();
653 let mut insert_ext = |fn_metadata: FunctionMetadata| {
654 extension_fns.insert(fn_metadata.ffqn.function_name.clone(), fn_metadata);
655 };
656 let mut insert_schedule = |fn_metadata: FunctionMetadata| {
657 schedule_fns.insert(fn_metadata.ffqn.function_name.clone(), fn_metadata);
658 };
659
660 let mut stub_fns = IndexMap::new();
661 for (
662 fun,
663 FunctionMetadata {
664 ffqn: _,
665 parameter_types,
666 return_type,
667 extension,
668 submittable: original_submittable,
669 },
670 ) in fns
671 {
672 assert!(
673 extension.is_none(),
674 "`exports_hierarchy` must not contain extensions"
675 );
676 let exported_fn_metadata = FunctionMetadata {
677 ffqn: FunctionFqn {
678 ifc_fqn: ifc_fqn.clone(),
679 function_name: fun.clone(),
680 },
681 parameter_types: parameter_types.clone(),
682 return_type: return_type.clone(),
683 extension: None,
684 submittable: false,
685 };
686 let ReturnType::Extendable(return_type) = return_type else {
687 unreachable!(
688 "all ExImLite exported functions must have their return type validated"
689 )
690 };
691
692 let fn_submit = FunctionMetadata {
694 ffqn: FunctionFqn {
695 ifc_fqn: obelisk_ext_ifc.clone(),
696 function_name: FnName::from(format!(
697 "{}{SUFFIX_FN_SUBMIT}",
698 exported_fn_metadata.ffqn.function_name
699 )),
700 },
701 parameter_types: {
702 let mut params =
703 Vec::with_capacity(exported_fn_metadata.parameter_types.len() + 1);
704 params.push(param_type_join_set.clone());
705 params.extend_from_slice(&exported_fn_metadata.parameter_types.0);
706 ParameterTypes(params)
707 },
708 return_type: return_type_execution_id.clone(),
709 extension: Some(FunctionExtension::Submit),
710 submittable: false,
711 };
712 insert_ext(fn_submit);
713
714 let fn_await_next = FunctionMetadata {
718 ffqn: FunctionFqn {
719 ifc_fqn: obelisk_ext_ifc.clone(),
720 function_name: FnName::from(format!(
721 "{}{SUFFIX_FN_AWAIT_NEXT}",
722 exported_fn_metadata.ffqn.function_name
723 )),
724 },
725 parameter_types: ParameterTypes(vec![param_type_join_set.clone()]),
726 return_type: {
727 ReturnType::detect(
728 TypeWrapper::Result {
729 ok: Some(Box::new(TypeWrapper::from(
730 return_type.type_wrapper_tl.clone(),
731 ))),
732 err: Some(Box::new(
733 await_next_extension_error_type_wrapper.clone(),
734 )),
735 },
736 StrVariant::from(format!(
737 "result<{original_ret}, await-next-extension-error>",
738 original_ret = exported_fn_metadata.return_type
739 )),
740 )
741 },
742 extension: Some(FunctionExtension::AwaitNext),
743 submittable: false,
744 };
745 insert_ext(fn_await_next);
746
747 let fn_get = FunctionMetadata {
749 ffqn: FunctionFqn {
750 ifc_fqn: obelisk_ext_ifc.clone(),
751 function_name: FnName::from(format!(
752 "{}{SUFFIX_FN_GET}",
753 exported_fn_metadata.ffqn.function_name
754 )),
755 },
756 parameter_types: ParameterTypes(vec![param_type_execution_id.clone()]),
757 return_type: {
758 ReturnType::detect(
759 TypeWrapper::Result {
760 ok: Some(Box::new(TypeWrapper::from(
761 return_type.type_wrapper_tl.clone(),
762 ))),
763 err: Some(Box::new(get_extension_error_type_wrapper.clone())),
764 },
765 StrVariant::from(format!(
766 "result<{}, get-extension-error>",
767 return_type.wit_type
768 )),
769 )
770 },
771 extension: Some(FunctionExtension::Get),
772 submittable: false,
773 };
774 insert_ext(fn_get);
775
776 if component_type != ComponentType::ActivityStub {
777 assert!(
778 original_submittable,
779 "original exported function must be submittable for components different from activity stubs"
780 );
781 let fn_schedule = FunctionMetadata {
783 ffqn: FunctionFqn {
784 ifc_fqn: obelisk_schedule_ifc.clone(),
785 function_name: FnName::from(format!(
786 "{}{SUFFIX_FN_SCHEDULE}",
787 exported_fn_metadata.ffqn.function_name
788 )),
789 },
790 parameter_types: {
791 let mut params =
792 Vec::with_capacity(exported_fn_metadata.parameter_types.len() + 1);
793 params.push(param_type_scheduled_at.clone());
794 params.extend_from_slice(&exported_fn_metadata.parameter_types.0);
795 ParameterTypes(params)
796 },
797 return_type: return_type_execution_id.clone(),
798 extension: Some(FunctionExtension::Schedule),
799 submittable: true,
800 };
801 insert_schedule(fn_schedule);
802 }
803
804 if component_type == ComponentType::ActivityStub {
805 let fn_stub = FunctionMetadata {
807 ffqn: FunctionFqn {
808 ifc_fqn: obelisk_stub_ifc.clone(),
809 function_name: FnName::from(format!(
810 "{}{SUFFIX_FN_STUB}",
811 exported_fn_metadata.ffqn.function_name
812 )),
813 },
814 parameter_types: {
815 let mut params = vec![param_type_execution_id.clone()];
816
817 params.push(ParameterType {
818 type_wrapper: TypeWrapper::from(
819 return_type.type_wrapper_tl.clone(),
820 ),
821 name: StrVariant::Static("execution-result"),
822 wit_type: return_type.wit_type.clone(),
823 });
824
825 ParameterTypes(params)
826 },
827 return_type: ReturnType::detect(
828 TypeWrapper::Result {
829 ok: None,
830 err: Some(Box::new(stub_error_type_wrapper.clone())),
831 },
832 StrVariant::Static("result<_, stub-error>"),
833 ),
834 extension: Some(FunctionExtension::Stub),
835 submittable: false,
836 };
837 stub_fns.insert(fn_stub.ffqn.function_name.clone(), fn_stub);
838 }
839 }
840 extensions.push((obelisk_ext_ifc, extension_fns));
841 schedules.push((obelisk_schedule_ifc, schedule_fns));
842 stubs.push((obelisk_stub_ifc, stub_fns));
843 }
844 for (ifc_fqn, fns) in extensions {
845 if !fns.is_empty() {
846 exports_hierarchy.push(PackageIfcFns {
847 ifc_fqn,
848 fns,
849 extension: true,
850 });
851 }
852 }
853 for (ifc_fqn, fns) in schedules {
854 if !fns.is_empty() {
855 exports_hierarchy.push(PackageIfcFns {
856 ifc_fqn,
857 fns,
858 extension: true,
859 });
860 }
861 }
862 for (ifc_fqn, fns) in stubs {
863 if !fns.is_empty() {
864 exports_hierarchy.push(PackageIfcFns {
865 ifc_fqn,
866 fns,
867 extension: true,
868 });
869 }
870 }
871 }
872
873 fn flatten(input: &[PackageIfcFns]) -> Vec<FunctionMetadata> {
874 input
875 .iter()
876 .flat_map(|pif| pif.fns.values().cloned())
877 .collect()
878 }
879}
880
881#[derive(Debug)]
883struct ExImLite {
884 imports: Vec<PackageIfcFns>,
885 exports: Vec<PackageIfcFns>,
887}
888
889#[derive(Clone, Copy, PartialEq)]
890enum ProcessingKind {
891 Imports,
892 ExportsOfActivityStub,
893 ExportsSubmittable,
894}
895impl ProcessingKind {
896 fn is_export(&self) -> bool {
897 matches!(
898 self,
899 ProcessingKind::ExportsSubmittable | ProcessingKind::ExportsOfActivityStub
900 )
901 }
902}
903
904fn populate_ifcs_with_compatible_fns(
905 resolve: &Resolve,
906 ifc_ids: impl Iterator<Item = InterfaceId>,
907 processing_kind: ProcessingKind,
908) -> Result<Vec<PackageIfcFns>, DecodeError> {
909 let mut vec = Vec::new();
910 let submittable = processing_kind == ProcessingKind::ExportsSubmittable;
911 for ifc in ifc_ids
912 .map(|ifc_id| {
913 resolve
914 .interfaces
915 .get(ifc_id)
916 .expect("`iter` must be derived from `resolve`")
917 })
918 .filter(|ifc| ifc.name.is_some())
920 {
921 let Some(package) = ifc
922 .package
923 .and_then(|pkg| resolve.packages.get(pkg))
924 .map(|p| &p.name)
925 else {
926 unreachable!("interface's package cannot be empty");
927 };
928 let Some(ifc_name) = ifc.name.as_deref() else {
929 unreachable!("inline interfaces already filterd out");
930 };
931 let pkg_fqn = PkgFqn {
932 namespace: package.namespace.clone(),
933 package_name: package.name.clone(),
934 version: package
935 .version
936 .as_ref()
937 .map(std::string::ToString::to_string),
938 };
939 let package_ext = pkg_fqn.split_ext().map(|(_, pkg_ext)| pkg_ext);
940 if processing_kind.is_export() && package_ext.is_some() {
941 return Err(DecodeError::new_without_source(format!(
942 "component is exporting obelisk extended package {pkg_fqn}"
943 )));
944 }
945 let ifc_fqn = if let Some(version) = &package.version {
946 format!(
947 "{namespace}:{name}/{ifc_name}@{version}",
948 namespace = package.namespace,
949 name = package.name
950 )
951 } else {
952 format!("{package}/{ifc_name}")
953 };
954 let ifc_fqn: Arc<str> = Arc::from(ifc_fqn);
955 let mut fns = IndexMap::new();
956 for (function_name, function) in &ifc.functions {
957 let ffqn = FunctionFqn::new_arc(ifc_fqn.clone(), Arc::from(function_name.clone()));
958 let return_type = if let Some(return_type) = function.result {
959 let mut printer = WitPrinter::default();
960 let wit_type = printer
961 .print_type_name(resolve, &return_type)
962 .ok()
963 .map_or(StrVariant::Static("unknown"), |()| {
964 StrVariant::from(printer.output.to_string())
965 });
966 Some(ReturnType::detect(
967 match TypeWrapper::from_wit_parser_type(resolve, &return_type) {
968 Ok(ok) => ok,
969 Err(err) => {
970 return Err(DecodeError::new_with_source(
971 format!("unsupported type in {ffqn}"),
972 err,
973 ));
974 }
975 },
976 wit_type,
977 ))
978 } else {
979 None
980 };
981
982 let return_type_valid = matches!(return_type, Some(ReturnType::Extendable(_)));
983
984 match (return_type, processing_kind.is_export()) {
985 (Some(return_type @ ReturnType::Extendable(_)), true)
986 | (Some(return_type), false) => {
987 let ffqn =
988 FunctionFqn::new_arc(ifc_fqn.clone(), Arc::from(function_name.clone()));
989 let parameter_types = ParameterTypes({
990 let mut params = Vec::new();
991 for Param {
992 name: param_name,
993 ty: param_ty,
994 span: _,
995 } in &function.params
996 {
997 let mut printer = WitPrinter::default();
998 let item = ParameterType {
999 type_wrapper: match TypeWrapper::from_wit_parser_type(
1000 resolve, param_ty,
1001 ) {
1002 Ok(ok) => ok,
1003 Err(err) => {
1004 return Err(DecodeError::new_with_source(
1005 format!("unsupported type in {ffqn}"),
1006 err,
1007 ));
1008 }
1009 },
1010 name: StrVariant::from(param_name.clone()),
1011 wit_type: printer
1012 .print_type_name(resolve, param_ty)
1013 .ok()
1014 .map_or(StrVariant::Static("unknown"), |()| {
1015 StrVariant::from(printer.output.to_string())
1016 }),
1017 };
1018 params.push(item);
1019 }
1020 params
1021 });
1022
1023 fns.insert(
1024 ffqn.function_name.clone(),
1025 FunctionMetadata {
1026 parameter_types,
1027 return_type,
1028 extension: {
1029 let guessed_fn_extension = FunctionExtension::iter()
1030 .find(|fn_ext| function_name.ends_with(fn_ext.suffix()));
1031 match (package_ext, guessed_fn_extension) {
1032 (None, _) => None,
1033 (Some(pkg_ext), Some(fn_ext)) if fn_ext.belongs_to(pkg_ext) => {
1034 Some(fn_ext)
1035 }
1036 _ => {
1037 return Err(DecodeError::new_without_source(format!(
1038 "wrong extension import {ffqn}"
1039 )));
1040 }
1041 }
1042 },
1043 ffqn,
1044 submittable,
1045 },
1046 );
1047 }
1048 (Some(return_type), true)
1049 if processing_kind.is_export()
1050 && !return_type_valid
1051 && !ifc_fqn.starts_with("wasi:http/incoming-handler@") =>
1052 {
1053 warn!("Ignoring export {ffqn} with unsupported return type {return_type:?}");
1056 }
1057 _ => {}
1058 }
1059 }
1060 if !fns.is_empty() {
1061 vec.push(PackageIfcFns {
1062 ifc_fqn: IfcFqnName::new_arc(ifc_fqn),
1063 fns,
1064 extension: package_ext.is_some(),
1065 });
1066 }
1067 }
1068 Ok(vec)
1069}
1070
1071#[cfg(test)]
1075pub(crate) fn strip_wasm_hash(filename: &str) -> String {
1076 let mut result = filename.to_string();
1077 if let Some(pos) = result.find('_').and_then(|_| {
1078 result
1079 .as_bytes()
1080 .windows(17)
1081 .position(|w| w[0] == b'_' && w[1..].iter().all(u8::is_ascii_hexdigit))
1082 }) {
1083 result.replace_range(pos..pos + 17, "");
1084 }
1085 result
1086}
1087
1088#[cfg(test)]
1089pub(crate) mod tests {
1090 use super::{populate_ifcs_with_compatible_fns, strip_wasm_hash};
1091 use crate::wasm_tools::{ExOrIm, ProcessingKind, WasmComponent, world_interfaces};
1092 use concepts::ComponentType;
1093 use rstest::rstest;
1094 use std::path::PathBuf;
1095 use wit_parser::decoding::DecodedWasm;
1096
1097 #[derive(Debug, Clone, Copy)]
1098 enum ExIm {
1099 Exports,
1100 ExportsExtended,
1101 Imports,
1102 }
1103
1104 #[rstest]
1105 #[case(test_programs_fibo_workflow_builder::TEST_PROGRAMS_FIBO_WORKFLOW)]
1106 #[case(test_programs_http_get_workflow_builder::TEST_PROGRAMS_HTTP_GET_WORKFLOW)]
1107 fn exports_imports(
1108 #[case] wasm_path: &str,
1109 #[values(ExIm::Exports, ExIm::ExportsExtended, ExIm::Imports)] exim: ExIm,
1110 ) {
1111 test_utils::set_up();
1112
1113 let wasm_path = PathBuf::from(wasm_path);
1114 let wasm_file = strip_wasm_hash(&wasm_path.file_name().unwrap().to_string_lossy());
1115 let component = WasmComponent::new(&wasm_path, ComponentType::Workflow).unwrap();
1116 match exim {
1117 ExIm::Exports => {
1118 let exports = component
1119 .exported_functions(false)
1120 .iter()
1121 .map(|fn_metadata| (fn_metadata.ffqn.to_string(), fn_metadata))
1122 .collect::<hashbrown::HashMap<_, _>>();
1123 insta::with_settings!({sort_maps => true, snapshot_suffix => format!("{wasm_file}_exports_noext")}, {insta::assert_json_snapshot!(exports)});
1124 }
1125 ExIm::ExportsExtended => {
1126 let exports = component
1127 .exported_functions(true)
1128 .iter()
1129 .map(|fn_metadata| (fn_metadata.ffqn.to_string(), fn_metadata))
1130 .collect::<hashbrown::HashMap<_, _>>();
1131 insta::with_settings!({sort_maps => true, snapshot_suffix => format!("{wasm_file}_exports_ext")}, {insta::assert_json_snapshot!(exports)});
1132 }
1133 ExIm::Imports => {
1134 let imports = component
1135 .imported_functions()
1136 .iter()
1137 .map(|fn_metadata| (fn_metadata.ffqn.to_string(), fn_metadata))
1138 .collect::<hashbrown::HashMap<_, _>>();
1139 insta::with_settings!({sort_maps => true, snapshot_suffix => format!("{wasm_file}_imports")}, {insta::assert_json_snapshot!(imports)});
1140 }
1141 }
1142 }
1143
1144 #[rstest]
1145 fn test_params(#[values(true, false)] exports: bool) {
1146 let wasm_path =
1147 PathBuf::from(test_programs_fibo_workflow_builder::TEST_PROGRAMS_FIBO_WORKFLOW);
1148 let wasm_file = strip_wasm_hash(&wasm_path.file_name().unwrap().to_string_lossy());
1149 let file = std::fs::File::open(&wasm_path).unwrap();
1150 let decoded = wit_parser::decoding::decode_reader(file).unwrap();
1151 let DecodedWasm::Component(resolve, world_id) = decoded else {
1152 panic!();
1153 };
1154 let world = resolve.worlds.get(world_id).expect("world must exist");
1155
1156 if exports {
1157 let exports = populate_ifcs_with_compatible_fns(
1158 &resolve,
1159 world_interfaces(world, ExOrIm::Exports),
1160 ProcessingKind::ExportsSubmittable,
1161 )
1162 .unwrap()
1163 .into_iter()
1164 .flat_map(|ifc| ifc.fns)
1165 .map(|(_fn_name, fn_metadata)| (fn_metadata.ffqn.to_string(), fn_metadata))
1166 .collect::<hashbrown::HashMap<_, _>>();
1167 insta::with_settings!({sort_maps => true, snapshot_suffix => format!("{wasm_file}_exports")}, {insta::assert_json_snapshot!(exports)});
1168 } else {
1169 let imports = populate_ifcs_with_compatible_fns(
1170 &resolve,
1171 world_interfaces(world, ExOrIm::Imports),
1172 ProcessingKind::Imports,
1173 )
1174 .unwrap()
1175 .into_iter()
1176 .flat_map(|ifc| ifc.fns)
1177 .map(|(_fn_name, fn_metadata)| (fn_metadata.ffqn.to_string(), fn_metadata))
1178 .collect::<hashbrown::HashMap<_, _>>();
1179 insta::with_settings!({ sort_maps => true, snapshot_suffix => format!("{wasm_file}_imports")}, {insta::assert_json_snapshot!(imports)});
1180 }
1181 }
1182
1183 #[rstest]
1184 #[case("fibo/activity", ComponentType::Activity)]
1185 #[case("fibo/workflow", ComponentType::Workflow)]
1186 #[case("fibo/webhook", ComponentType::WebhookEndpoint)]
1187 #[case("stub/activity", ComponentType::ActivityStub)]
1188 fn test_wit_folder_parsing(#[case] path: &'static str, #[case] component_type: ComponentType) {
1189 use std::fmt::Write;
1190
1191 let workspace_dir = PathBuf::from(
1192 std::env::var("CARGO_WORKSPACE_DIR")
1193 .as_deref()
1194 .unwrap_or("."),
1195 )
1196 .canonicalize()
1197 .unwrap();
1198 let wasm_component = WasmComponent::new_from_wit_folder(
1199 workspace_dir
1200 .join("crates/testing/test-programs")
1201 .join(path)
1202 .join("wit"),
1203 component_type,
1204 )
1205 .unwrap();
1206
1207 let mut pkgs_to_wits: Vec<_> = wasm_component
1208 .exported_extension_wits()
1209 .unwrap()
1210 .into_iter()
1211 .map(|(pkg_fqn, wit)| (pkg_fqn.to_string(), wit))
1212 .collect();
1213 pkgs_to_wits.sort_by(|(pkg_fqn, _), (pkg_fqn2, _)| pkg_fqn.cmp(pkg_fqn2));
1214
1215 let mut snapshot = String::new();
1216 for (pkg_fqn, wit) in pkgs_to_wits {
1217 write!(&mut snapshot, "{pkg_fqn}\n{wit}\n\n").unwrap();
1218 }
1219
1220 insta::with_settings!({ snapshot_suffix => format!("{path}")}, {insta::assert_snapshot!(snapshot)});
1221 }
1222
1223 #[test]
1224 fn test_new_from_wit_string() {
1225 let wit = "
1226 package a:b;
1227 interface c {
1228 fn: func(p1: string) -> result;
1229 }
1230 world z {
1231 export c;
1232 }
1233 ";
1234 let user_wasm_component =
1235 WasmComponent::new_from_wit_string(wit, ComponentType::Activity).unwrap();
1236 let exports = user_wasm_component.exported_functions(false).to_vec();
1237 insta::assert_debug_snapshot!(exports);
1238 }
1239
1240 const CANCELLABLE_WIT: &str = "
1241 package a:b;
1242 interface c {
1243 run-cancellable: func(p1: string) -> result;
1244 }
1245 world z {
1246 export c;
1247 }
1248 ";
1249
1250 #[test]
1251 fn cancellable_workflow_export_is_accepted() {
1252 WasmComponent::new_from_wit_string(CANCELLABLE_WIT, ComponentType::Workflow).unwrap();
1253 }
1254
1255 #[rstest]
1256 #[case(ComponentType::Activity)]
1257 #[case(ComponentType::WebhookEndpoint)]
1258 fn cancellable_suffix_rejected_on_non_workflow(#[case] wrong_type: ComponentType) {
1259 let err = WasmComponent::new_from_wit_string(CANCELLABLE_WIT, wrong_type).unwrap_err();
1260 assert!(
1261 err.to_string()
1262 .contains("-cancellable is reserved for workflow exports"),
1263 "unexpected error: {err}"
1264 );
1265 }
1266}