diff --git a/crates/rune-macros/src/any.rs b/crates/rune-macros/src/any.rs index ac5e01f83..f56dabba8 100644 --- a/crates/rune-macros/src/any.rs +++ b/crates/rune-macros/src/any.rs @@ -138,7 +138,7 @@ pub(crate) fn expand_install_with( match &input.data { syn::Data::Struct(st) => { - expand_struct_install_with(cx, installers, st, tokens, attr)?; + expand_struct_install_with(cx, installers, ident, st, tokens, attr)?; } syn::Data::Enum(en) => { expand_enum_install_with(cx, installers, ident, en, tokens, attr, generics)?; @@ -164,6 +164,7 @@ pub(crate) fn expand_install_with( fn expand_struct_install_with( cx: &Context, installers: &mut Vec, + ident: &syn::Ident, st: &syn::DataStruct, tokens: &Tokens, attr: &TypeAttr, @@ -217,13 +218,32 @@ fn expand_struct_install_with( match &st.fields { syn::Fields::Named(fields) => { + let constructor = attr + .constructor + .then(|| { + let args = fields.named.iter().map(|f| { + let ident = f.ident.as_ref().expect("named fields must have an Ident"); + let typ = &f.ty; + quote!(#ident: #typ) + }); + + let field_names = fields.named.iter().map(|f| f.ident.as_ref()); + + quote!(|#(#args),*| { + #ident { + #(#field_names),* + } + }) + }) + .map(|c| quote!(.constructor(#c)?)); + let fields = fields.named.iter().flat_map(|f| { let ident = f.ident.as_ref()?; Some(syn::LitStr::new(&ident.to_string(), ident.span())) }); installers.push(quote! { - module.type_meta::()?.make_named_struct(&[#(#fields,)*])?.static_docs(&#docs); + module.type_meta::()?.make_named_struct(&[#(#fields,)*])?#constructor.static_docs(&#docs); }); } syn::Fields::Unnamed(fields) => { diff --git a/crates/rune-macros/src/context.rs b/crates/rune-macros/src/context.rs index f0c54a574..9b7a55c7c 100644 --- a/crates/rune-macros/src/context.rs +++ b/crates/rune-macros/src/context.rs @@ -72,6 +72,8 @@ pub(crate) struct TypeAttr { pub(crate) parse: ParseKind, /// `#[rune(item = )]`. pub(crate) item: Option, + /// `#[rune(constructor)]`. + pub(crate) constructor: bool, /// Parsed documentation. pub(crate) docs: Vec, } @@ -441,6 +443,8 @@ impl Context { // Parse `#[rune(install_with = )]` meta.input.parse::()?; attr.install_with = Some(parse_path_compat(meta.input)?); + } else if meta.path == CONSTRUCTOR { + attr.constructor = true; } else { return Err(syn::Error::new_spanned( &meta.path, diff --git a/crates/rune/src/compile/context.rs b/crates/rune/src/compile/context.rs index 4b099daa4..6bf4e6db6 100644 --- a/crates/rune/src/compile/context.rs +++ b/crates/rune/src/compile/context.rs @@ -429,17 +429,51 @@ impl Context { let kind = if let Some(spec) = &ty.spec { match spec { - TypeSpecification::Struct(fields) => meta::Kind::Struct { - fields: match fields { - Fields::Named(fields) => meta::Fields::Named(meta::FieldsNamed { - fields: fields.iter().copied().map(Box::::from).collect(), - }), - Fields::Unnamed(args) => meta::Fields::Unnamed(*args), - Fields::Empty => meta::Fields::Empty, - }, - constructor: None, - parameters, - }, + TypeSpecification::Struct(fields) => { + let constructor = match &ty.constructor { + Some(c) => { + let hash = Hash::type_hash(&item); + + let signature = meta::Signature { + #[cfg(feature = "doc")] + is_async: false, + #[cfg(feature = "doc")] + args: Some(match fields { + Fields::Named(names) => names.len(), + Fields::Unnamed(args) => *args, + Fields::Empty => 0, + }), + #[cfg(feature = "doc")] + return_type: Some(ty.hash), + #[cfg(feature = "doc")] + argument_types: Box::from([]), + }; + + self.insert_native_fn(hash, c)?; + Some(signature) + } + None => None, + }; + + meta::Kind::Struct { + fields: match fields { + Fields::Named(fields) => meta::Fields::Named(meta::FieldsNamed { + fields: fields + .iter() + .copied() + .enumerate() + .map(|(position, name)| { + (Box::::from(name), meta::FieldMeta { position }) + }) + .collect(), + }), + Fields::Unnamed(args) => meta::Fields::Unnamed(*args), + Fields::Empty => meta::Fields::Empty, + }, + constructor, + parameters, + } + } TypeSpecification::Enum(en) => { for (index, variant) in en.variants.iter().enumerate() { let Some(fields) = &variant.fields else { @@ -495,7 +529,13 @@ impl Context { fields: names .iter() .copied() - .map(Box::::from) + .enumerate() + .map(|(position, name)| { + ( + Box::::from(name), + meta::FieldMeta { position }, + ) + }) .collect(), }) } @@ -874,7 +914,14 @@ impl Context { index, fields: match fields { Fields::Named(fields) => meta::Fields::Named(meta::FieldsNamed { - fields: fields.iter().copied().map(Box::::from).collect(), + fields: fields + .iter() + .copied() + .enumerate() + .map(|(position, name)| { + (Box::::from(name), meta::FieldMeta { position }) + }) + .collect(), }), Fields::Unnamed(args) => meta::Fields::Unnamed(*args), Fields::Empty => meta::Fields::Empty, diff --git a/crates/rune/src/compile/context_error.rs b/crates/rune/src/compile/context_error.rs index 10e64bccd..6f6f4cb06 100644 --- a/crates/rune/src/compile/context_error.rs +++ b/crates/rune/src/compile/context_error.rs @@ -77,6 +77,9 @@ pub enum ContextError { ConflictingVariant { item: ItemBuf, }, + ConstructorConflict { + type_info: TypeInfo, + }, ValueError { error: VmError, }, @@ -196,6 +199,12 @@ impl fmt::Display for ContextError { ContextError::ConflictingVariant { item } => { write!(f, "Variant with `{item}` already exists")?; } + ContextError::ConstructorConflict { type_info } => { + write!( + f, + "Constructor for type `{type_info}` has already been registered" + )?; + } ContextError::ValueError { error } => { write!(f, "Error when converting to constant value: {error}")?; } diff --git a/crates/rune/src/compile/meta.rs b/crates/rune/src/compile/meta.rs index d7dc8a913..ad540c00c 100644 --- a/crates/rune/src/compile/meta.rs +++ b/crates/rune/src/compile/meta.rs @@ -3,7 +3,7 @@ use core::fmt; use crate::no_std::borrow::Cow; -use crate::no_std::collections::HashSet; +use crate::no_std::collections::HashMap; use crate::no_std::path::Path; use crate::no_std::prelude::*; @@ -287,7 +287,14 @@ pub struct Import { #[non_exhaustive] pub struct FieldsNamed { /// Fields associated with the type. - pub(crate) fields: HashSet>, + pub(crate) fields: HashMap, FieldMeta>, +} + +/// Metadata for a single named field. +#[derive(Debug, Clone)] +pub struct FieldMeta { + /// Position of the field in its containing type declaration. + pub(crate) position: usize, } /// Item and the module that the item belongs to. diff --git a/crates/rune/src/compile/v1/assemble.rs b/crates/rune/src/compile/v1/assemble.rs index 284d395d8..1439e49fd 100644 --- a/crates/rune/src/compile/v1/assemble.rs +++ b/crates/rune/src/compile/v1/assemble.rs @@ -1957,7 +1957,9 @@ fn expr_object<'hir>( ) -> compile::Result> { let guard = cx.scopes.child(span)?; - for assign in hir.assignments { + let base = cx.scopes.total(span)?; + + for assign in hir.assignments.iter() { expr(cx, &assign.assign, Needs::Value)?.apply(cx)?; cx.scopes.alloc(&span)?; } @@ -1976,6 +1978,10 @@ fn expr_object<'hir>( hir::ExprObjectKind::StructVariant { hash } => { cx.asm.push(Inst::StructVariant { hash, slot }, span); } + hir::ExprObjectKind::ExternalType { hash, args } => { + reorder_field_assignments(cx, hir, base, span)?; + cx.asm.push(Inst::Call { hash, args }, span); + } hir::ExprObjectKind::Anonymous => { cx.asm.push(Inst::Object { slot }, span); } @@ -1991,6 +1997,56 @@ fn expr_object<'hir>( Ok(Asm::top(span)) } +/// Reorder the position of the field assignments on the stack so that they +/// match the expected argument order when invoking the constructor function. +fn reorder_field_assignments<'hir>( + cx: &mut Ctxt<'_, 'hir, '_>, + hir: &hir::ExprObject<'hir>, + base: usize, + span: &dyn Spanned, +) -> compile::Result<()> { + let mut order = Vec::with_capacity(hir.assignments.len()); + + for assign in hir.assignments { + let Some(position) = assign.position else { + return Err(compile::Error::msg( + span, + format_args!("Missing position for field assignment {}", assign.key.1), + )); + }; + + order.push(position); + } + + for a in 0..hir.assignments.len() { + loop { + let Some(&b) = order.get(a) else { + return Err(compile::Error::msg( + span, + "Order out-of-bounds", + )); + }; + + if a == b { + break; + } + + order.swap(a, b); + + let (Some(a), Some(b)) = (base.checked_add(a), base.checked_add(b)) else { + return Err(compile::Error::msg( + span, + "Field repositioning out-of-bounds", + )); + }; + + cx.asm.push(Inst::Swap { a, b }, span); + } + } + + Ok(()) +} + /// Assemble a range expression. #[instrument(span = span)] fn expr_range<'hir>( diff --git a/crates/rune/src/hir/hir.rs b/crates/rune/src/hir/hir.rs index 41072ae40..1d4493203 100644 --- a/crates/rune/src/hir/hir.rs +++ b/crates/rune/src/hir/hir.rs @@ -580,6 +580,7 @@ pub(crate) enum ExprObjectKind { UnitStruct { hash: Hash }, Struct { hash: Hash }, StructVariant { hash: Hash }, + ExternalType { hash: Hash, args: usize }, Anonymous, } @@ -601,6 +602,8 @@ pub(crate) struct FieldAssign<'hir> { pub(crate) key: (Span, &'hir str), /// The assigned expression of the field. pub(crate) assign: Expr<'hir>, + /// The position of the field in its containing type declaration. + pub(crate) position: Option, } /// A literal vector. diff --git a/crates/rune/src/hir/lowering.rs b/crates/rune/src/hir/lowering.rs index db8ceeb3e..5af2c4586 100644 --- a/crates/rune/src/hir/lowering.rs +++ b/crates/rune/src/hir/lowering.rs @@ -330,7 +330,7 @@ pub(crate) fn expr_object<'hir>( let span = ast; let mut keys_dup = HashMap::new(); - let assignments = &*iter!(&ast.assignments, |(ast, _)| { + let assignments = &mut *iter!(&ast.assignments, |(ast, _)| { let key = object_key(cx, &ast.key)?; if let Some(existing) = keys_dup.insert(key.1, key.0) { @@ -362,25 +362,31 @@ pub(crate) fn expr_object<'hir>( hir::FieldAssign { key: (key.0.span(), key.1), assign, + position: None, } }); - let check_object_fields = |fields: &HashSet<_>, item: &Item| { + let mut check_object_fields = |fields: &HashMap<_, meta::FieldMeta>, item: &Item| { let mut fields = fields.clone(); - for assign in assignments { - if !fields.remove(assign.key.1) { - return Err(compile::Error::new( - assign.key.0, - ErrorKind::LitObjectNotField { - field: assign.key.1.into(), - item: item.to_owned(), - }, - )); - } + for assign in assignments.iter_mut() { + match fields.remove(assign.key.1) { + Some(field_meta) => { + assign.position = Some(field_meta.position); + } + None => { + return Err(compile::Error::new( + assign.key.0, + ErrorKind::LitObjectNotField { + field: assign.key.1.into(), + item: item.to_owned(), + }, + )); + } + }; } - if let Some(field) = fields.into_iter().next() { + if let Some(field) = fields.into_keys().next() { return Err(compile::Error::new( span, ErrorKind::LitObjectMissingField { @@ -405,15 +411,23 @@ pub(crate) fn expr_object<'hir>( fields: meta::Fields::Empty, .. } => { - check_object_fields(&HashSet::new(), item)?; + check_object_fields(&HashMap::new(), item)?; hir::ExprObjectKind::UnitStruct { hash: meta.hash } } meta::Kind::Struct { fields: meta::Fields::Named(st), + constructor, .. } => { check_object_fields(&st.fields, item)?; - hir::ExprObjectKind::Struct { hash: meta.hash } + + match constructor { + Some(_) => hir::ExprObjectKind::ExternalType { + hash: meta.hash, + args: st.fields.len(), + }, + None => hir::ExprObjectKind::Struct { hash: meta.hash }, + } } meta::Kind::Variant { fields: meta::Fields::Named(st), @@ -1167,7 +1181,7 @@ fn pat<'hir>(cx: &mut Ctxt<'hir, '_, '_>, ast: &ast::Pat) -> compile::Result = st.fields.keys().cloned().collect(); for binding in bindings.iter() { if !fields.remove(binding.key()) { diff --git a/crates/rune/src/module.rs b/crates/rune/src/module.rs index 8e76d9efc..a89dc85eb 100644 --- a/crates/rune/src/module.rs +++ b/crates/rune/src/module.rs @@ -116,6 +116,8 @@ pub(crate) struct ModuleType { pub(crate) type_info: TypeInfo, /// The specification for the type. pub(crate) spec: Option, + /// Handler to use if this type can be constructed through a regular function call. + pub(crate) constructor: Option>, /// Documentation for the type. pub(crate) docs: Docs, } @@ -485,6 +487,7 @@ where { docs: &'a mut Docs, spec: &'a mut Option, + constructor: &'a mut Option>, item: &'a Item, _marker: PhantomData<&'a mut T>, } @@ -555,6 +558,24 @@ where }) } + /// Register a constructor method for the current type. + pub fn constructor(self, constructor: F) -> Result + where + F: Function, + { + if self.constructor.is_some() { + return Err(ContextError::ConstructorConflict { + type_info: T::type_info(), + }); + } + + *self.constructor = Some(Arc::new(move |stack, args| { + constructor.fn_call(stack, args) + })); + + Ok(self) + } + fn make_struct(self, fields: Fields) -> Result { let old = self.spec.replace(TypeSpecification::Struct(fields)); diff --git a/crates/rune/src/module/module.rs b/crates/rune/src/module/module.rs index 7af830558..b5519e497 100644 --- a/crates/rune/src/module/module.rs +++ b/crates/rune/src/module/module.rs @@ -197,6 +197,7 @@ impl Module { type_parameters, type_info, spec: None, + constructor: None, docs: Docs::EMPTY, }); @@ -207,6 +208,7 @@ impl Module { Ok(TypeMut { docs: &mut ty.docs, spec: &mut ty.spec, + constructor: &mut ty.constructor, item: &ty.item, _marker: PhantomData, }) @@ -229,6 +231,7 @@ impl Module { Ok(TypeMut { docs: &mut ty.docs, spec: &mut ty.spec, + constructor: &mut ty.constructor, item: &ty.item, _marker: PhantomData, }) diff --git a/crates/rune/src/query/query.rs b/crates/rune/src/query/query.rs index cf6a6f2e1..594274e44 100644 --- a/crates/rune/src/query/query.rs +++ b/crates/rune/src/query/query.rs @@ -9,7 +9,7 @@ use crate::no_std::sync::Arc; use crate::ast::{Span, Spanned}; use crate::compile::context::ContextMeta; use crate::compile::ir; -use crate::compile::meta; +use crate::compile::meta::{self, FieldMeta}; use crate::compile::{ self, CompileVisitor, ComponentRef, Doc, DynLocation, ErrorKind, ImportStep, IntoComponent, Item, ItemBuf, ItemId, ItemMeta, Located, Location, ModId, ModMeta, Names, Pool, Prelude, @@ -1190,11 +1190,11 @@ impl<'a, 'arena> Query<'a, 'arena> { ast::Fields::Empty => meta::Fields::Empty, ast::Fields::Unnamed(tuple) => meta::Fields::Unnamed(tuple.len()), ast::Fields::Named(st) => { - let mut fields = HashSet::new(); + let mut fields = HashMap::with_capacity(st.len()); - for (ast::Field { name, .. }, _) in st { + for (position, (ast::Field { name, .. }, _)) in st.iter().enumerate() { let name = name.resolve(cx)?; - fields.insert(name.into()); + fields.insert(name.into(), FieldMeta { position }); } meta::Fields::Named(meta::FieldsNamed { fields }) diff --git a/crates/rune/src/runtime/inst.rs b/crates/rune/src/runtime/inst.rs index 4ca5e7049..7930a780a 100644 --- a/crates/rune/src/runtime/inst.rs +++ b/crates/rune/src/runtime/inst.rs @@ -452,6 +452,15 @@ pub enum Inst { /// Offset to swap value from. offset: usize, }, + /// Swap two values on the stack using their offsets relative to the current + /// stack frame. + #[musli(packed)] + Swap { + /// Offset to the first value. + a: usize, + /// Offset to the second value. + b: usize, + }, /// Pop the current stack frame and restore the instruction pointer from it. /// /// The stack frame will be cleared, and the value on the top of the stack diff --git a/crates/rune/src/runtime/stack.rs b/crates/rune/src/runtime/stack.rs index d18821e07..590dcffce 100644 --- a/crates/rune/src/runtime/stack.rs +++ b/crates/rune/src/runtime/stack.rs @@ -323,6 +323,22 @@ impl Stack { Ok(self.drain(count)?.collect::>()) } + /// Swap the value at position a with the value at position b. + pub(crate) fn swap(&mut self, a: usize, b: usize) -> Result<(), StackError> { + let a = self + .stack_bottom + .checked_add(a) + .filter(|&n| n < self.stack.len()) + .ok_or(StackError)?; + let b = self + .stack_bottom + .checked_add(b) + .filter(|&n| n < self.stack.len()) + .ok_or(StackError)?; + self.stack.swap(a, b); + Ok(()) + } + /// Modify stack top by subtracting the given count from it while checking /// that it is in bounds of the stack. /// diff --git a/crates/rune/src/runtime/unit.rs b/crates/rune/src/runtime/unit.rs index 9a8dafc8d..8c04bc006 100644 --- a/crates/rune/src/runtime/unit.rs +++ b/crates/rune/src/runtime/unit.rs @@ -184,12 +184,12 @@ impl Unit { .map(|keys| &keys[..]) } - /// Lookup runt-time information for the given type hash. + /// Lookup run-time information for the given type hash. pub(crate) fn lookup_rtti(&self, hash: Hash) -> Option<&Arc> { self.logic.rtti.get(&hash) } - /// Lookup variant runt-time information for the given variant hash. + /// Lookup variant run-time information for the given variant hash. pub(crate) fn lookup_variant_rtti(&self, hash: Hash) -> Option<&Arc> { self.logic.variant_rtti.get(&hash) } diff --git a/crates/rune/src/runtime/vm.rs b/crates/rune/src/runtime/vm.rs index 781b1a65d..a918043aa 100644 --- a/crates/rune/src/runtime/vm.rs +++ b/crates/rune/src/runtime/vm.rs @@ -1567,6 +1567,13 @@ impl Vm { VmResult::Ok(()) } + /// Swap two values on the stack. + #[cfg_attr(feature = "bench", inline(never))] + fn op_swap(&mut self, a: usize, b: usize) -> VmResult<()> { + vm_try!(self.stack.swap(a, b)); + VmResult::Ok(()) + } + /// Perform a jump operation. #[cfg_attr(feature = "bench", inline(never))] fn op_jump(&mut self, jump: usize) -> VmResult<()> { @@ -3100,6 +3107,9 @@ impl Vm { Inst::Dup => { vm_try!(self.op_dup()); } + Inst::Swap { a, b } => { + vm_try!(self.op_swap(a, b)); + } Inst::Replace { offset } => { vm_try!(self.op_replace(offset)); } diff --git a/crates/rune/src/tests/external_constructor.rs b/crates/rune/src/tests/external_constructor.rs index 5c5d5d68d..4ff72fb6c 100644 --- a/crates/rune/src/tests/external_constructor.rs +++ b/crates/rune/src/tests/external_constructor.rs @@ -76,3 +76,140 @@ fn construct_enum() { let output: Enum = from_value(output).unwrap(); assert_eq!(output, Enum::Output(2 * 3 * 4)); } + +/// Tests constructing an external struct from within Rune, and receiving +/// external structs as an argument. +#[test] +fn construct_struct() { + #[derive(Debug, Any, PartialEq, Eq)] + #[rune(constructor)] + struct Request { + #[rune(get)] + url: String, + } + + #[derive(Debug, Any, PartialEq, Eq)] + #[rune(constructor)] + struct Response { + #[rune(get, set)] + status_code: u32, + #[rune(get, set)] + body: String, + #[rune(get, set)] + headers: Headers, + } + + #[derive(Debug, Any, Clone, PartialEq, Eq)] + #[rune(constructor)] + struct Headers { + #[rune(get, set)] + content_type: String, + #[rune(get, set)] + content_length: u32, + } + + fn make_module() -> Result { + let mut module = Module::new(); + module.ty::()?; + module.ty::()?; + module.ty::()?; + Ok(module) + } + + let m = make_module().expect("Module should be buildable"); + + let mut context = Context::new(); + context.install(m).expect("Context should build"); + let runtime = Arc::new(context.runtime()); + + let mut sources = sources! { + entry => { + pub fn main(req) { + let content_type = "text/plain"; + + // Field order has been purposefully scrambled here, to test + // that they can be given in any order and still compile + // correctly. + let rsp = match req.url { + "/" => Response { + status_code: 200, + body: "ok", + headers: Headers { + content_length: 2, + content_type, + }, + }, + "/account" => Response { + headers: Headers { + content_type, + content_length: 12, + }, + body: "unauthorized", + status_code: 401, + }, + _ => Response { + body: "not found", + status_code: 404, + headers: Headers { + content_type, + content_length: 9, + }, + } + }; + + rsp + } + } + }; + + let unit = prepare(&mut sources) + .with_context(&context) + .build() + .expect("Unit should build"); + + let mut vm = Vm::new(runtime, Arc::new(unit)); + + for (req, rsp) in vec![ + ( + Request { url: "/".into() }, + Response { + status_code: 200, + body: "ok".into(), + headers: Headers { + content_type: "text/plain".into(), + content_length: 2, + }, + }, + ), + ( + Request { + url: "/account".into(), + }, + Response { + status_code: 401, + body: "unauthorized".into(), + headers: Headers { + content_type: "text/plain".into(), + content_length: 12, + }, + }, + ), + ( + Request { + url: "/cart".into(), + }, + Response { + status_code: 404, + body: "not found".into(), + headers: Headers { + content_type: "text/plain".into(), + content_length: 9, + }, + }, + ), + ] { + let output = vm.call(["main"], (req,)).unwrap(); + let output: Response = from_value(output).unwrap(); + assert_eq!(output, rsp); + } +} diff --git a/examples/examples/external_struct.rs b/examples/examples/external_struct.rs new file mode 100644 index 000000000..045a6ec1d --- /dev/null +++ b/examples/examples/external_struct.rs @@ -0,0 +1,85 @@ +use std::sync::Arc; + +use rune::runtime::Vm; +use rune::termcolor::{ColorChoice, StandardStream}; +use rune::{Any, ContextError, Diagnostics, Module}; + +#[derive(Default, Debug, Any, PartialEq, Eq)] +struct Request { + #[rune(get, set)] + url: String, +} + +#[derive(Default, Debug, Any, PartialEq, Eq)] +#[rune(constructor)] +struct Response { + #[rune(get, set)] + status_code: usize, + #[rune(get, set)] + body: String, +} + +fn main() -> rune::Result<()> { + let m = module()?; + + let mut context = rune_modules::default_context()?; + context.install(m)?; + let runtime = Arc::new(context.runtime()); + + let mut sources = rune::sources! { + entry => { + pub fn main(req) { + let rsp = match req.url { + "/" => Response { + status_code: 200, + body: "ok", + }, + _ => Response { + status_code: 400, + body: "not found", + } + }; + + rsp + } + } + }; + + let mut diagnostics = Diagnostics::new(); + + let result = rune::prepare(&mut sources) + .with_context(&context) + .with_diagnostics(&mut diagnostics) + .build(); + + if !diagnostics.is_empty() { + let mut writer = StandardStream::stderr(ColorChoice::Always); + diagnostics.emit(&mut writer, &sources)?; + } + + let unit = result?; + + let mut vm = Vm::new(runtime, Arc::new(unit)); + + let output = vm.call(["main"], (Request { url: "/".into() },))?; + let output: Response = rune::from_value(output)?; + println!("{:?}", output); + + let output = vm.call( + ["main"], + (Request { + url: "/account".into(), + },), + )?; + let output: Response = rune::from_value(output)?; + println!("{:?}", output); + + Ok(()) +} + +pub fn module() -> Result { + let mut module = Module::new(); + module.ty::()?; + module.ty::()?; + Ok(module) +}