use crate::code_block::{Arg, CodeBlock};
use crate::lang::CodeLang;
use crate::spec::annotation_spec::AnnotationSpec;
use crate::spec::modifiers::{
ConstructorDelegationStyle, DeclarationContext, Modifiers, Visibility,
};
use crate::spec::parameter_spec::ParameterSpec;
use crate::type_name::TypeName;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct WhereConstraint {
pub(crate) subject: TypeName,
pub(crate) bounds: Vec<TypeName>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum WhereClauseStyle {
Inline,
WhereBlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamListStyle {
Tupled,
Curried,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionSignatureStyle {
Merged,
Split,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum TypeParamKind {
Constructor1,
Constructor2,
Raw(String),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TypeParamSpec {
pub(crate) name: String,
pub(crate) bounds: Vec<TypeName>,
#[serde(default)]
pub(crate) kind: Option<TypeParamKind>,
#[serde(default)]
pub(crate) is_lifetime: bool,
#[serde(default)]
pub(crate) context_bounds: Vec<TypeName>,
}
impl TypeParamSpec {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
bounds: Vec::new(),
kind: None,
is_lifetime: false,
context_bounds: Vec::new(),
}
}
pub fn lifetime(name: &str) -> Self {
Self {
name: name.to_string(),
bounds: Vec::new(),
kind: None,
is_lifetime: true,
context_bounds: Vec::new(),
}
}
pub fn with_bound(mut self, bound: TypeName) -> Self {
self.bounds.push(bound);
self
}
pub fn with_kind(mut self, kind: TypeParamKind) -> Self {
self.kind = Some(kind);
self
}
pub fn with_context_bound(mut self, bound: TypeName) -> Self {
self.context_bounds.push(bound);
self
}
}
pub fn render_type_params(
params: &[TypeParamSpec],
lang: &dyn CodeLang,
args: &mut Vec<Arg>,
) -> String {
if params.is_empty() {
return String::new();
}
let generic = lang.generic_syntax();
let constraint_kw = generic.constraint_keyword;
let constraint_sep = generic.constraint_separator;
let mut fmt = String::from(generic.open);
let mut first = true;
for tp in params.iter().filter(|p| p.is_lifetime) {
if !first {
fmt.push_str(", ");
}
fmt.push_str(&tp.name);
if !tp.bounds.is_empty() {
fmt.push_str(constraint_kw);
for (j, bound) in tp.bounds.iter().enumerate() {
if j > 0 {
fmt.push_str(constraint_sep);
}
fmt.push_str("%T");
args.push(Arg::TypeName(bound.clone()));
}
}
first = false;
}
for tp in params.iter().filter(|p| !p.is_lifetime) {
if !first {
fmt.push_str(", ");
}
fmt.push_str(&tp.name);
if let Some(ref kind) = tp.kind {
fmt.push_str(&lang.render_type_param_kind(kind));
}
if !tp.bounds.is_empty() {
fmt.push_str(constraint_kw);
for (j, bound) in tp.bounds.iter().enumerate() {
if j > 0 {
fmt.push_str(constraint_sep);
}
fmt.push_str("%T");
args.push(Arg::TypeName(bound.clone()));
}
}
let ctx_kw = generic.context_bound_keyword;
for ctx_bound in &tp.context_bounds {
fmt.push_str(ctx_kw);
fmt.push_str("%T");
args.push(Arg::TypeName(ctx_bound.clone()));
}
first = false;
}
fmt.push_str(generic.close);
fmt
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FunSpec {
pub(crate) name: String,
pub(crate) params: Vec<ParameterSpec>,
pub(crate) return_type: Option<TypeName>,
pub(crate) body: Option<CodeBlock>,
pub(crate) modifiers: Modifiers,
pub(crate) doc: Vec<String>,
pub(crate) type_params: Vec<TypeParamSpec>,
pub(crate) annotations: Vec<CodeBlock>,
pub(crate) annotation_specs: Vec<AnnotationSpec>,
pub(crate) receiver: Option<ParameterSpec>,
pub(crate) suffixes: Vec<String>,
pub(crate) delegation: Option<CodeBlock>,
#[serde(default)]
pub(crate) where_constraints: Vec<WhereConstraint>,
}
impl FunSpec {
pub fn builder(name: &str) -> FunSpecBuilder {
FunSpecBuilder {
name: name.to_string(),
params: Vec::new(),
return_type: None,
body: None,
modifiers: Modifiers::default(),
doc: Vec::new(),
type_params: Vec::new(),
annotations: Vec::new(),
annotation_specs: Vec::new(),
receiver: None,
suffixes: Vec::new(),
delegation: None,
where_constraints: Vec::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn emit(
&self,
lang: &dyn CodeLang,
ctx: DeclarationContext,
) -> Result<CodeBlock, crate::error::SigilStitchError> {
let mut cb = CodeBlock::builder();
let emit_doc = || -> Option<String> {
if self.doc.is_empty() || lang.doc_comment_inside_body() {
return None;
}
let doc_lines: Vec<&str> = self.doc.iter().map(|s| s.as_str()).collect();
Some(lang.render_doc_comment(&doc_lines))
};
if lang.doc_before_annotations()
&& let Some(doc_str) = emit_doc()
{
cb.add("%L", doc_str);
cb.add_line();
}
for spec in &self.annotation_specs {
cb.add_code(spec.emit(lang)?);
cb.add_line();
}
for ann in &self.annotations {
cb.add_code(ann.clone());
cb.add_line();
}
if !lang.doc_before_annotations()
&& let Some(doc_str) = emit_doc()
{
cb.add("%L", doc_str);
cb.add_line();
}
if lang.function_syntax().function_signature_style == FunctionSignatureStyle::Split {
return self.emit_split_signature(cb, lang);
}
let vis = lang.render_visibility(self.modifiers.visibility, ctx);
let fn_kw = if self.modifiers.is_constructor {
lang.function_syntax().constructor_keyword
} else {
lang.function_keyword(ctx)
};
let mut sig = String::new();
let mut sig_args: Vec<Arg> = Vec::new();
sig.push_str(vis);
if self.modifiers.is_abstract {
sig.push_str(lang.function_syntax().abstract_keyword);
}
if self.modifiers.is_static {
sig.push_str("static ");
}
if self.modifiers.is_override {
sig.push_str("override ");
}
if self.modifiers.is_async {
sig.push_str(lang.function_syntax().async_keyword);
}
if lang.type_decl_syntax().return_type_is_prefix
&& let Some(ret) = &self.return_type
{
sig.push_str("%T");
sig_args.push(Arg::TypeName(ret.clone()));
sig.push(' ');
}
if !fn_kw.is_empty() {
sig.push_str(fn_kw);
sig.push(' ');
}
if let Some(recv) = &self.receiver {
sig.push('(');
sig.push_str(&lang.escape_reserved(&recv.name));
sig.push_str(lang.type_decl_syntax().type_annotation_separator);
sig.push_str("%T");
sig_args.push(Arg::TypeName(recv.param_type.clone()));
sig.push_str(") ");
}
sig.push_str(&self.name);
let tp_str = render_type_params(&self.type_params, lang, &mut sig_args);
sig.push_str(&tp_str);
if lang.function_syntax().param_list_style == ParamListStyle::Curried {
if !self.params.is_empty() {
sig.push(' ');
sig.push_str("%L");
let params_block = self.build_curried_params_block(lang)?;
sig_args.push(Arg::Code(params_block));
}
} else {
sig.push('(');
sig.push_str("%L");
let params_block = self.build_params_block(lang)?;
sig_args.push(Arg::Code(params_block));
sig.push(')');
}
for s in &self.suffixes {
sig.push(' ');
sig.push_str(s);
}
if !lang.type_decl_syntax().return_type_is_prefix
&& let Some(ret) = &self.return_type
{
sig.push_str(lang.function_syntax().return_type_separator);
sig.push_str("%T");
sig_args.push(Arg::TypeName(ret.clone()));
}
let delegation_in_body = if let Some(deleg) = &self.delegation {
if lang.function_syntax().constructor_delegation_style
== ConstructorDelegationStyle::Signature
{
sig.push_str(" : %L");
sig_args.push(Arg::Code(deleg.clone()));
false
} else {
true
}
} else {
false
};
if let Some(body) = &self.body {
self.emit_where_and_open(&mut sig, &mut sig_args, lang);
cb.add(&sig, sig_args);
cb.add_line();
cb.add("%>", ());
if !self.doc.is_empty() && lang.doc_comment_inside_body() {
let doc_lines: Vec<&str> = self.doc.iter().map(|s| s.as_str()).collect();
let doc_str = lang.render_doc_comment(&doc_lines);
cb.add("%L", doc_str);
cb.add_line();
}
if delegation_in_body && let Some(deleg) = &self.delegation {
cb.add_statement("%L", deleg.clone());
}
cb.add_code(body.clone());
if !body.ends_with_newline_or_block_close() {
cb.add_line();
}
cb.add("%<", ());
let close = lang.block_syntax().block_close;
if !close.is_empty() {
cb.add(close, ());
cb.add_line();
}
} else {
let empty = lang.function_syntax().empty_body;
if !empty.is_empty() {
self.emit_where_and_open(&mut sig, &mut sig_args, lang);
cb.add(&sig, sig_args);
cb.add_line();
cb.add("%>", ());
if !self.doc.is_empty() && lang.doc_comment_inside_body() {
let doc_lines: Vec<&str> = self.doc.iter().map(|s| s.as_str()).collect();
let doc_str = lang.render_doc_comment(&doc_lines);
cb.add("%L", doc_str);
cb.add_line();
}
if delegation_in_body && let Some(deleg) = &self.delegation {
cb.add_statement("%L", deleg.clone());
}
cb.add_statement(empty, ());
cb.add("%<", ());
let close = lang.block_syntax().block_close;
if !close.is_empty() {
cb.add(close, ());
cb.add_line();
}
} else {
if lang.block_syntax().uses_semicolons {
sig.push(';');
}
cb.add(&sig, sig_args);
cb.add_line();
}
}
cb.build()
}
fn build_params_block(
&self,
lang: &dyn CodeLang,
) -> Result<CodeBlock, crate::error::SigilStitchError> {
let mut pb = CodeBlock::builder();
for (i, param) in self.params.iter().enumerate() {
if i > 0 {
pb.add(",%W", ());
}
param.emit_into(&mut pb, lang);
}
pb.build()
}
fn build_curried_params_block(
&self,
lang: &dyn CodeLang,
) -> Result<CodeBlock, crate::error::SigilStitchError> {
let mut pb = CodeBlock::builder();
for (i, param) in self.params.iter().enumerate() {
if i > 0 {
pb.add(" ", ());
}
pb.add("(", ());
param.emit_into(&mut pb, lang);
pb.add(")", ());
}
pb.build()
}
fn emit_where_and_open(&self, sig: &mut String, sig_args: &mut Vec<Arg>, lang: &dyn CodeLang) {
if !self.where_constraints.is_empty()
&& lang.function_syntax().where_clause_style == WhereClauseStyle::WhereBlock
{
emit_where_block(sig, sig_args, &self.where_constraints, lang);
}
sig.push_str(lang.fun_block_open());
}
fn emit_split_signature(
&self,
mut cb: crate::code_block::CodeBlockBuilder,
lang: &dyn CodeLang,
) -> Result<CodeBlock, crate::error::SigilStitchError> {
let resolve = |_module: &str, name: &str| name.to_string();
let context = lang.render_type_context(&self.type_params);
let mut type_parts: Vec<String> = Vec::new();
for param in &self.params {
let t = param.param_type.render(80, &resolve).unwrap_or_default();
type_parts.push(t);
}
if let Some(ret) = &self.return_type {
let r = ret.render(80, &resolve).unwrap_or_default();
type_parts.push(r);
}
if !type_parts.is_empty() {
let type_sig = format!("{} :: {}{}", self.name, context, type_parts.join(" -> "));
cb.add("%L", type_sig);
cb.add_line();
}
let mut def = String::new();
def.push_str(&self.name);
for param in &self.params {
def.push(' ');
def.push_str(&lang.escape_reserved(¶m.name));
}
def.push_str(lang.block_syntax().block_open);
if let Some(body) = &self.body {
cb.add(&def, ());
cb.add_line();
cb.add("%>", ());
cb.add_code(body.clone());
if !body.ends_with_newline_or_block_close() {
cb.add_line();
}
cb.add("%<", ());
let close = lang.block_syntax().block_close;
if !close.is_empty() {
cb.add(close, ());
cb.add_line();
}
} else {
let empty = lang.function_syntax().empty_body;
if !empty.is_empty() {
cb.add(&def, ());
cb.add_line();
cb.add("%>", ());
cb.add_statement(empty, ());
cb.add("%<", ());
let close = lang.block_syntax().block_close;
if !close.is_empty() {
cb.add(close, ());
cb.add_line();
}
} else {
}
}
cb.build()
}
}
pub(crate) fn emit_where_block(
fmt: &mut String,
args: &mut Vec<Arg>,
constraints: &[WhereConstraint],
lang: &dyn CodeLang,
) {
let generic = lang.generic_syntax();
let constraint_sep = generic.constraint_separator;
let indent = lang.block_syntax().indent_unit;
fmt.push_str("\nwhere\n");
for (i, wc) in constraints.iter().enumerate() {
if i > 0 {
fmt.push('\n');
}
fmt.push_str(indent);
fmt.push_str("%T");
args.push(Arg::TypeName(wc.subject.clone()));
fmt.push_str(lang.generic_syntax().constraint_keyword);
for (j, bound) in wc.bounds.iter().enumerate() {
if j > 0 {
fmt.push_str(constraint_sep);
}
fmt.push_str("%T");
args.push(Arg::TypeName(bound.clone()));
}
fmt.push(',');
}
}
#[derive(Debug)]
pub struct FunSpecBuilder {
name: String,
params: Vec<ParameterSpec>,
return_type: Option<TypeName>,
body: Option<CodeBlock>,
modifiers: Modifiers,
doc: Vec<String>,
type_params: Vec<TypeParamSpec>,
annotations: Vec<CodeBlock>,
annotation_specs: Vec<AnnotationSpec>,
receiver: Option<ParameterSpec>,
suffixes: Vec<String>,
delegation: Option<CodeBlock>,
where_constraints: Vec<WhereConstraint>,
}
impl FunSpecBuilder {
pub fn add_param(mut self, param: ParameterSpec) -> Self {
self.params.push(param);
self
}
pub fn returns(mut self, ret: TypeName) -> Self {
self.return_type = Some(ret);
self
}
pub fn body(mut self, body: CodeBlock) -> Self {
self.body = Some(body);
self
}
pub fn visibility(mut self, vis: Visibility) -> Self {
self.modifiers.visibility = vis;
self
}
pub fn is_async(mut self) -> Self {
self.modifiers.is_async = true;
self
}
pub fn is_static(mut self) -> Self {
self.modifiers.is_static = true;
self
}
pub fn is_abstract(mut self) -> Self {
self.modifiers.is_abstract = true;
self
}
pub fn is_override(mut self) -> Self {
self.modifiers.is_override = true;
self
}
pub fn is_constructor(mut self) -> Self {
self.modifiers.is_constructor = true;
self
}
pub fn doc(mut self, line: &str) -> Self {
self.doc.push(line.to_string());
self
}
pub fn add_type_param(mut self, tp: TypeParamSpec) -> Self {
self.type_params.push(tp);
self
}
pub fn annotation(mut self, ann: CodeBlock) -> Self {
self.annotations.push(ann);
self
}
pub fn annotate(mut self, spec: AnnotationSpec) -> Self {
self.annotation_specs.push(spec);
self
}
pub fn receiver(mut self, recv: ParameterSpec) -> Self {
self.receiver = Some(recv);
self
}
pub fn suffix(mut self, s: &str) -> Self {
self.suffixes.push(s.to_string());
self
}
pub fn delegation(mut self, call: CodeBlock) -> Self {
self.delegation = Some(call);
self
}
pub fn add_where_constraint(mut self, subject: TypeName, bounds: Vec<TypeName>) -> Self {
self.where_constraints
.push(WhereConstraint { subject, bounds });
self
}
pub fn where_bound(mut self, param_name: &str, bound: TypeName) -> Self {
if let Some(wc) = self
.where_constraints
.iter_mut()
.find(|wc| wc.subject.simple_name() == Some(param_name))
{
wc.bounds.push(bound);
} else {
self.where_constraints.push(WhereConstraint {
subject: TypeName::primitive(param_name),
bounds: vec![bound],
});
}
self
}
pub fn build(self) -> Result<FunSpec, crate::error::SigilStitchError> {
snafu::ensure!(
!self.name.is_empty(),
crate::error::EmptyNameSnafu {
builder: "FunSpecBuilder",
}
);
Ok(FunSpec {
name: self.name,
params: self.params,
return_type: self.return_type,
body: self.body,
modifiers: self.modifiers,
doc: self.doc,
type_params: self.type_params,
annotations: self.annotations,
annotation_specs: self.annotation_specs,
receiver: self.receiver,
suffixes: self.suffixes,
delegation: self.delegation,
where_constraints: self.where_constraints,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::rust_lang::RustLang;
use crate::lang::typescript::TypeScript;
fn emit_fun_ts(spec: &FunSpec, ctx: DeclarationContext) -> String {
let lang = TypeScript::new();
let block = spec.emit(&lang, ctx).unwrap();
let imports = crate::import::ImportGroup::new();
let mut renderer = crate::code_renderer::CodeRenderer::new(&lang, &imports, 80);
renderer.render(&block).unwrap()
}
fn emit_fun_rs(spec: &FunSpec, ctx: DeclarationContext) -> String {
let lang = RustLang::new();
let block = spec.emit(&lang, ctx).unwrap();
let imports = crate::import::ImportGroup::new();
let mut renderer = crate::code_renderer::CodeRenderer::new(&lang, &imports, 80);
renderer.render(&block).unwrap()
}
#[test]
fn test_ts_simple_function() {
let body = CodeBlock::of("console.log(name)", ()).unwrap();
let fun = FunSpec::builder("greet")
.add_param(ParameterSpec::new("name", TypeName::primitive("string")).unwrap())
.returns(TypeName::primitive("void"))
.body(body)
.build()
.unwrap();
let output = emit_fun_ts(&fun, DeclarationContext::TopLevel);
assert!(output.contains("function greet(name: string): void {"));
assert!(output.contains("console.log(name)"));
assert!(output.contains("}"));
}
#[test]
fn test_ts_async_method() {
let body = CodeBlock::of("return db.find(id)", ()).unwrap();
let fun = FunSpec::builder("getUser")
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::generic(
TypeName::primitive("Promise"),
vec![TypeName::primitive("User")],
))
.is_async()
.visibility(Visibility::Public)
.body(body)
.build()
.unwrap();
let output = emit_fun_ts(&fun, DeclarationContext::Member);
assert!(output.contains("public async getUser(id: string): Promise<User> {"));
}
#[test]
fn test_ts_abstract_method() {
let fun = FunSpec::builder("validate")
.is_abstract()
.returns(TypeName::primitive("boolean"))
.build()
.unwrap();
let output = emit_fun_ts(&fun, DeclarationContext::Member);
assert!(output.contains("abstract validate(): boolean;"));
}
#[test]
fn test_rust_simple_function() {
let body = CodeBlock::of("a + b", ()).unwrap();
let fun = FunSpec::builder("add")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("a", TypeName::primitive("i32")).unwrap())
.add_param(ParameterSpec::new("b", TypeName::primitive("i32")).unwrap())
.returns(TypeName::primitive("i32"))
.body(body)
.build()
.unwrap();
let output = emit_fun_rs(&fun, DeclarationContext::TopLevel);
assert!(output.contains("pub fn add(a: i32, b: i32) -> i32 {"));
assert!(output.contains("a + b"));
}
#[test]
fn test_fun_with_type_params() {
let tp = TypeParamSpec::new("T").with_bound(TypeName::primitive("Serializable"));
let body = CodeBlock::of("return JSON.stringify(value)", ()).unwrap();
let fun = FunSpec::builder("serialize")
.add_type_param(tp)
.add_param(ParameterSpec::new("value", TypeName::primitive("T")).unwrap())
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
let output = emit_fun_ts(&fun, DeclarationContext::TopLevel);
assert!(output.contains("function serialize<T extends Serializable>(value: T): string {"));
}
#[test]
fn test_build_empty_name_errors() {
let result = FunSpec::builder("").build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("'name' must not be empty")
);
}
#[test]
fn test_where_clause_rust_function() {
let fun = FunSpec::builder("process")
.add_type_param(TypeParamSpec::new("T"))
.add_type_param(TypeParamSpec::new("U"))
.add_where_constraint(
TypeName::primitive("T"),
vec![TypeName::primitive("Clone"), TypeName::primitive("Send")],
)
.add_where_constraint(TypeName::primitive("U"), vec![TypeName::primitive("Debug")])
.add_param(ParameterSpec::new("a", TypeName::primitive("T")).unwrap())
.add_param(ParameterSpec::new("b", TypeName::primitive("U")).unwrap())
.body(CodeBlock::of("todo!()", ()).unwrap())
.build()
.unwrap();
let output = emit_fun_rs(&fun, DeclarationContext::TopLevel);
assert!(
output.contains("fn process<T, U>(a: T, b: U)"),
"sig: {output}"
);
assert!(
output.contains("where\n T: Clone + Send,\n U: Debug,"),
"where: {output}"
);
assert!(output.contains(" {"), "block_open: {output}");
}
#[test]
fn test_where_clause_ts_inline_ignored() {
let fun = FunSpec::builder("process")
.add_type_param(TypeParamSpec::new("T"))
.add_where_constraint(
TypeName::primitive("T"),
vec![TypeName::primitive("Serializable")],
)
.body(CodeBlock::of("return value", ()).unwrap())
.build()
.unwrap();
let output = emit_fun_ts(&fun, DeclarationContext::TopLevel);
assert!(
!output.contains("where"),
"TS should not emit where: {output}"
);
assert!(output.contains("function process<T>("), "sig: {output}");
}
#[test]
fn test_where_bound_convenience() {
let fun = FunSpec::builder("example")
.add_type_param(TypeParamSpec::new("T"))
.where_bound("T", TypeName::primitive("Clone"))
.where_bound("T", TypeName::primitive("Send"))
.body(CodeBlock::of("todo!()", ()).unwrap())
.build()
.unwrap();
let output = emit_fun_rs(&fun, DeclarationContext::TopLevel);
assert!(
output.contains("where\n T: Clone + Send,"),
"where: {output}"
);
}
#[test]
fn test_type_param_kind_none_unchanged() {
let fun = FunSpec::builder("foo")
.add_type_param(TypeParamSpec::new("T"))
.body(CodeBlock::of("return null", ()).unwrap())
.build()
.unwrap();
let output = emit_fun_ts(&fun, DeclarationContext::TopLevel);
assert!(output.contains("function foo<T>()"), "unchanged: {output}");
}
#[test]
fn test_type_param_with_kind_default_no_output() {
let fun = FunSpec::builder("apply")
.add_type_param(TypeParamSpec::new("F").with_kind(TypeParamKind::Constructor1))
.body(CodeBlock::of("todo!()", ()).unwrap())
.build()
.unwrap();
let output = emit_fun_rs(&fun, DeclarationContext::TopLevel);
assert!(
output.contains("fn apply<F>()"),
"default renders no kind suffix: {output}"
);
}
#[test]
fn test_lifetime_params_before_type_params() {
let fun = FunSpec::builder("longest")
.add_type_param(TypeParamSpec::new("T"))
.add_type_param(TypeParamSpec::lifetime("'a"))
.add_param(
ParameterSpec::new(
"x",
TypeName::reference_with_lifetime(TypeName::primitive("str"), "'a"),
)
.unwrap(),
)
.add_param(
ParameterSpec::new(
"y",
TypeName::reference_with_lifetime(TypeName::primitive("str"), "'a"),
)
.unwrap(),
)
.returns(TypeName::reference_with_lifetime(
TypeName::primitive("str"),
"'a",
))
.body(CodeBlock::of("x", ()).unwrap())
.build()
.unwrap();
let output = emit_fun_rs(&fun, DeclarationContext::TopLevel);
assert!(
output.contains("fn longest<'a, T>("),
"lifetime first: {output}"
);
assert!(
output.contains("x: &'a str"),
"lifetime ref param: {output}"
);
assert!(
output.contains("-> &'a str"),
"lifetime ref return: {output}"
);
}
}