use crate::code_node::{CodeNode, parts_args_to_nodes};
use crate::import::ImportRef;
use crate::lang::CodeLang;
use crate::type_name::TypeName;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Specifier {
Type,
Name,
StringLit,
VerbatimStr,
Literal,
Comment,
}
impl Specifier {
pub fn from_format_char(ch: char) -> Option<Self> {
match ch {
'T' => Some(Self::Type),
'N' => Some(Self::Name),
'S' => Some(Self::StringLit),
'V' => Some(Self::VerbatimStr),
'L' => Some(Self::Literal),
'R' => Some(Self::Comment),
_ => None,
}
}
pub fn format_char(self) -> char {
match self {
Self::Type => 'T',
Self::Name => 'N',
Self::StringLit => 'S',
Self::VerbatimStr => 'V',
Self::Literal => 'L',
Self::Comment => 'R',
}
}
pub fn all() -> &'static [Self] {
&[
Self::Type,
Self::Name,
Self::StringLit,
Self::VerbatimStr,
Self::Literal,
Self::Comment,
]
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub(crate) enum FormatPart {
Literal(String),
Arg(Specifier),
Wrap,
Indent,
Dedent,
StatementBegin,
StatementEnd,
Newline,
BlockOpen(String),
BlockClose(String),
BranchClose(String),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Arg {
TypeName(TypeName),
Name(String),
StringLit(String),
VerbatimStr(String),
Literal(String),
Code(CodeBlock),
Comment(String),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CodeBlock {
pub(crate) nodes: Vec<CodeNode>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CodeFragment {
block: CodeBlock,
}
impl CodeFragment {
pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
let nodes = format_to_nodes(format, args.into_args())?;
validate_balanced_indent_markers(&nodes)?;
Ok(Self {
block: CodeBlock { nodes },
})
}
pub fn into_code_block(self) -> CodeBlock {
self.block
}
}
impl CodeBlock {
pub fn builder() -> CodeBlockBuilder {
CodeBlockBuilder::new()
}
pub fn nodes_mut(&mut self) -> &mut Vec<CodeNode> {
&mut self.nodes
}
pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
let mut builder = CodeBlockBuilder::new();
builder.add(format, args);
builder.build()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn fragment(
format: &str,
args: impl IntoArgs,
) -> Result<CodeFragment, crate::error::SigilStitchError> {
CodeFragment::of(format, args)
}
pub fn ends_with_newline_or_block_close(&self) -> bool {
fn check_last(nodes: &[CodeNode]) -> bool {
match nodes.last() {
Some(CodeNode::Newline | CodeNode::BlockClose(_)) => true,
Some(CodeNode::Sequence(children)) => check_last(children),
Some(CodeNode::Nested(inner)) => check_last(&inner.nodes),
_ => false,
}
}
check_last(&self.nodes)
}
#[doc(hidden)]
pub fn __sigil_trim_trailing_newline(mut self) -> Self {
fn trim(nodes: &mut Vec<CodeNode>) -> bool {
match nodes.last_mut() {
Some(CodeNode::Newline) => {
nodes.pop();
true
}
Some(CodeNode::Sequence(children)) => trim(children),
Some(CodeNode::Nested(inner)) => trim(&mut inner.nodes),
_ => false,
}
}
trim(&mut self.nodes);
self
}
pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
crate::import_collector::walk_nodes(&self.nodes, out);
}
pub fn render_standalone(
&self,
lang: &dyn CodeLang,
width: usize,
) -> Result<String, crate::error::SigilStitchError> {
let imports = crate::import::ImportGroup::new();
let mut renderer = crate::code_renderer::CodeRenderer::new(lang, &imports, width);
renderer.render(self)
}
}
#[derive(Debug)]
pub struct CodeBlockBuilder {
nodes: Vec<CodeNode>,
indent_depth: i32,
block_stack: Vec<String>,
errors: Vec<crate::error::SigilStitchError>,
}
impl CodeBlockBuilder {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
indent_depth: 0,
block_stack: Vec::new(),
errors: Vec::new(),
}
}
pub fn add(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
let new_nodes = match format_to_nodes(format, args.into_args()) {
Ok(nodes) => nodes,
Err(err) => {
self.errors.push(err);
return self;
}
};
self.nodes.extend(new_nodes);
self
}
pub fn add_statement(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
self.nodes.push(CodeNode::StatementBegin);
self.add(format, args);
self.nodes.push(CodeNode::StatementEnd);
self.nodes.push(CodeNode::Newline);
self
}
pub fn begin_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
let condition = format.to_string();
self.block_stack.push(condition.clone());
self.add(format, args);
self.nodes.push(CodeNode::BlockOpen(condition));
self.nodes.push(CodeNode::Newline);
self.nodes.push(CodeNode::Indent);
self.indent_depth += 1;
self
}
pub fn next_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
let condition = self.block_stack.last().cloned().unwrap_or_default();
self.nodes.push(CodeNode::Dedent);
self.indent_depth -= 1;
self.nodes.push(CodeNode::BranchClose(condition));
self.add(format, args);
let new_condition = format.to_string();
self.nodes.push(CodeNode::BlockOpen(new_condition));
self.nodes.push(CodeNode::Newline);
self.nodes.push(CodeNode::Indent);
self.indent_depth += 1;
self
}
pub fn end_control_flow(&mut self) -> &mut Self {
let condition = self.block_stack.pop().unwrap_or_default();
self.nodes.push(CodeNode::Dedent);
self.indent_depth -= 1;
self.nodes.push(CodeNode::BlockClose(condition));
self.nodes.push(CodeNode::Newline);
self
}
pub fn end_control_flow_no_newline(&mut self) -> &mut Self {
let condition = self.block_stack.pop().unwrap_or_default();
self.nodes.push(CodeNode::Dedent);
self.indent_depth -= 1;
self.nodes.push(CodeNode::BlockClose(condition));
self
}
pub fn end_control_flow_with_semicolon(&mut self) -> &mut Self {
let condition = self.block_stack.pop().unwrap_or_default();
self.nodes.push(CodeNode::Dedent);
self.indent_depth -= 1;
self.nodes.push(CodeNode::BlockClose(condition));
self.nodes.push(CodeNode::StatementEnd);
self.nodes.push(CodeNode::Newline);
self
}
pub fn add_line(&mut self) -> &mut Self {
self.nodes.push(CodeNode::Newline);
self
}
pub fn add_comment(&mut self, text: &str) -> &mut Self {
self.nodes.push(CodeNode::Comment(text.to_string()));
self.nodes.push(CodeNode::Newline);
self
}
pub fn add_attribute(&mut self, text: &str) -> &mut Self {
self.nodes.push(CodeNode::Attribute(text.to_string()));
self.nodes.push(CodeNode::Newline);
self
}
pub fn add_code(&mut self, block: CodeBlock) -> &mut Self {
self.nodes.push(CodeNode::Nested(block));
self
}
pub fn add_fragment(&mut self, fragment: CodeFragment) -> &mut Self {
self.add_code(fragment.into_code_block())
}
pub fn build(self) -> Result<CodeBlock, crate::error::SigilStitchError> {
if let Some(err) = self.errors.into_iter().next() {
return Err(err);
}
if self.indent_depth != 0 {
return Err(crate::error::SigilStitchError::UnbalancedIndent {
depth: self.indent_depth,
});
}
validate_balanced_indent_markers(&self.nodes)?;
validate_no_unresolved_indent_markers(&self.nodes)?;
Ok(CodeBlock { nodes: self.nodes })
}
pub fn build_unwrap(self) -> CodeBlock {
self.build().unwrap()
}
}
impl Default for CodeBlockBuilder {
fn default() -> Self {
Self::new()
}
}
fn format_to_nodes(
format: &str,
args: Vec<Arg>,
) -> Result<Vec<CodeNode>, crate::error::SigilStitchError> {
let parsed = parse_format(format)?;
let consuming_specifiers: Vec<String> = parsed
.iter()
.filter_map(|p| match p {
FormatPart::Arg(s) => Some(format!("%{}", s.format_char())),
_ => None,
})
.collect();
let expected_args = consuming_specifiers.len();
if expected_args != args.len() {
let actual_arg_kinds: Vec<String> = args.iter().map(arg_kind_name).collect();
return Err(crate::error::SigilStitchError::FormatArgCount {
format: format.to_string(),
expected: expected_args,
actual: args.len(),
expected_specifiers: consuming_specifiers,
actual_arg_kinds,
});
}
let nodes = parts_args_to_nodes(&parsed, &args);
validate_no_unresolved_indent_markers(&nodes)?;
Ok(nodes)
}
pub(crate) fn validate_balanced_indent_markers(
nodes: &[CodeNode],
) -> Result<(), crate::error::SigilStitchError> {
fn walk(nodes: &[CodeNode], depth: &mut i32) -> Result<(), crate::error::SigilStitchError> {
for node in nodes {
match node {
CodeNode::Indent => *depth += 1,
CodeNode::Dedent => *depth -= 1,
CodeNode::Nested(block) => walk(&block.nodes, depth)?,
CodeNode::Sequence(children) => walk(children, depth)?,
_ => {}
}
}
Ok(())
}
let mut depth = 0;
walk(nodes, &mut depth)?;
if depth != 0 {
return Err(crate::error::SigilStitchError::UnbalancedIndent { depth });
}
Ok(())
}
pub(crate) fn validate_no_unresolved_indent_markers(
nodes: &[CodeNode],
) -> Result<(), crate::error::SigilStitchError> {
fn check_text(text: &str, context: &str) -> Result<(), crate::error::SigilStitchError> {
for marker in ["%>", "%<"] {
if text.contains(marker) {
return Err(crate::error::SigilStitchError::UnresolvedIndentMarker {
marker: marker.to_string(),
context: context.to_string(),
});
}
}
Ok(())
}
for node in nodes {
match node {
CodeNode::Literal(text) => check_text(text, "format literal")?,
CodeNode::InlineLiteral(text) => check_text(text, "%L literal")?,
CodeNode::Nested(block) => validate_no_unresolved_indent_markers(&block.nodes)?,
CodeNode::Sequence(children) => validate_no_unresolved_indent_markers(children)?,
_ => {}
}
}
Ok(())
}
fn arg_kind_name(arg: &Arg) -> String {
match arg {
Arg::TypeName(_) => "TypeName".to_string(),
Arg::Name(_) => "Name".to_string(),
Arg::StringLit(_) => "StringLit".to_string(),
Arg::VerbatimStr(_) => "VerbatimStr".to_string(),
Arg::Literal(_) => "Literal".to_string(),
Arg::Code(_) => "Code".to_string(),
Arg::Comment(_) => "Comment".to_string(),
}
}
fn parse_format(format: &str) -> Result<Vec<FormatPart>, crate::error::SigilStitchError> {
let mut parts = Vec::new();
let mut current_literal = String::new();
let mut chars = format.char_indices().peekable();
while let Some(&(_, ch)) = chars.peek() {
if ch == '%' {
chars.next();
if let Some(&(_, spec)) = chars.peek() {
chars.next();
let part = match spec {
'W' => Some(FormatPart::Wrap),
'>' => Some(FormatPart::Indent),
'<' => Some(FormatPart::Dedent),
'[' => Some(FormatPart::StatementBegin),
']' => Some(FormatPart::StatementEnd),
'%' => {
current_literal.push('%');
continue;
}
_ => match Specifier::from_format_char(spec) {
Some(s) => Some(FormatPart::Arg(s)),
None => {
return Err(crate::error::SigilStitchError::InvalidFormatSpecifier {
format: format.to_string(),
specifier: spec,
});
}
},
};
if let Some(part) = part {
if !current_literal.is_empty() {
parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
}
parts.push(part);
}
}
} else if ch == '\n' {
chars.next();
if !current_literal.is_empty() {
parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
}
parts.push(FormatPart::Newline);
} else {
chars.next();
current_literal.push(ch);
}
}
if !current_literal.is_empty() {
parts.push(FormatPart::Literal(current_literal));
}
Ok(parts)
}
pub trait IntoArgs {
fn into_args(self) -> Vec<Arg>;
}
impl IntoArgs for () {
fn into_args(self) -> Vec<Arg> {
Vec::new()
}
}
impl IntoArgs for TypeName {
fn into_args(self) -> Vec<Arg> {
vec![Arg::TypeName(self)]
}
}
impl IntoArgs for &str {
fn into_args(self) -> Vec<Arg> {
vec![Arg::Literal(self.to_string())]
}
}
impl IntoArgs for String {
fn into_args(self) -> Vec<Arg> {
vec![Arg::Literal(self)]
}
}
impl IntoArgs for CodeBlock {
fn into_args(self) -> Vec<Arg> {
vec![Arg::Code(self)]
}
}
impl IntoArgs for CodeFragment {
fn into_args(self) -> Vec<Arg> {
vec![Arg::Code(self.into_code_block())]
}
}
impl IntoArgs for Vec<Arg> {
fn into_args(self) -> Vec<Arg> {
self
}
}
pub struct NameArg(pub String);
impl IntoArgs for NameArg {
fn into_args(self) -> Vec<Arg> {
vec![Arg::Name(self.0)]
}
}
pub struct StringLitArg(pub String);
impl IntoArgs for StringLitArg {
fn into_args(self) -> Vec<Arg> {
vec![Arg::StringLit(self.0)]
}
}
pub struct VerbatimStrArg(pub String);
impl IntoArgs for VerbatimStrArg {
fn into_args(self) -> Vec<Arg> {
vec![Arg::VerbatimStr(self.0)]
}
}
pub struct CommentArg(pub String);
impl IntoArgs for CommentArg {
fn into_args(self) -> Vec<Arg> {
vec![Arg::Comment(self.0)]
}
}
impl From<TypeName> for Arg {
fn from(tn: TypeName) -> Self {
Arg::TypeName(tn)
}
}
impl From<&str> for Arg {
fn from(s: &str) -> Self {
Arg::Literal(s.to_string())
}
}
impl From<String> for Arg {
fn from(s: String) -> Self {
Arg::Literal(s)
}
}
impl From<CodeBlock> for Arg {
fn from(cb: CodeBlock) -> Self {
Arg::Code(cb)
}
}
impl From<CodeFragment> for Arg {
fn from(fragment: CodeFragment) -> Self {
Arg::Code(fragment.into_code_block())
}
}
impl From<NameArg> for Arg {
fn from(n: NameArg) -> Self {
Arg::Name(n.0)
}
}
impl From<StringLitArg> for Arg {
fn from(s: StringLitArg) -> Self {
Arg::StringLit(s.0)
}
}
impl From<VerbatimStrArg> for Arg {
fn from(s: VerbatimStrArg) -> Self {
Arg::VerbatimStr(s.0)
}
}
impl From<CommentArg> for Arg {
fn from(s: CommentArg) -> Self {
Arg::Comment(s.0)
}
}
macro_rules! impl_into_args_tuple {
($($idx:tt $T:ident),+) => {
impl<$($T: Into<Arg>),+> IntoArgs for ($($T,)+) {
fn into_args(self) -> Vec<Arg> {
vec![$(self.$idx.into()),+]
}
}
};
}
impl_into_args_tuple!(0 A);
impl_into_args_tuple!(0 A, 1 B);
impl_into_args_tuple!(0 A, 1 B, 2 C);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
#[cfg(test)]
mod tests {
use super::*;
use crate::code_node::CodeNode;
use crate::lang::typescript::TypeScript;
#[test]
fn test_parse_all_specifiers() {
let parts = parse_format("hello %T world %N %S %L %W %> %< %[ %]").unwrap();
assert!(parts.contains(&FormatPart::Arg(Specifier::Type)));
assert!(parts.contains(&FormatPart::Arg(Specifier::Name)));
assert!(parts.contains(&FormatPart::Arg(Specifier::StringLit)));
assert!(parts.contains(&FormatPart::Arg(Specifier::Literal)));
assert!(parts.contains(&FormatPart::Wrap));
assert!(parts.contains(&FormatPart::Indent));
assert!(parts.contains(&FormatPart::Dedent));
assert!(parts.contains(&FormatPart::StatementBegin));
assert!(parts.contains(&FormatPart::StatementEnd));
}
#[test]
fn test_parse_literal_percent() {
let parts = parse_format("100%%").unwrap();
assert_eq!(parts, vec![FormatPart::Literal("100%".to_string())]);
}
#[test]
fn test_parse_empty() {
let parts = parse_format("").unwrap();
assert!(parts.is_empty());
}
#[test]
fn test_parse_newlines() {
let parts = parse_format("line1\nline2").unwrap();
assert_eq!(
parts,
vec![
FormatPart::Literal("line1".to_string()),
FormatPart::Newline,
FormatPart::Literal("line2".to_string()),
]
);
}
#[test]
fn test_builder_add_statement() {
let mut b = CodeBlock::builder();
b.add_statement("const x = %L", "42");
let block = b.build().unwrap();
assert!(!block.is_empty());
let has_stmt_begin = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::StatementBegin));
let has_stmt_end = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::StatementEnd));
assert!(has_stmt_begin);
assert!(has_stmt_end);
}
#[test]
fn test_builder_control_flow() {
let mut b = CodeBlock::builder();
b.begin_control_flow("if (x > 0)", ());
b.add_statement("return x", ());
b.end_control_flow();
let block = b.build().unwrap();
assert!(!block.is_empty());
}
#[test]
fn test_builder_unbalanced_control_flow() {
let mut b = CodeBlock::builder();
b.begin_control_flow("if (x)", ());
b.add_statement("y()", ());
let result = b.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unbalanced"));
}
#[test]
fn test_mismatched_arg_count() {
let mut b = CodeBlock::builder();
b.add("%T", ());
let result = b.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("expects 1 args but got 0")
);
}
#[test]
fn test_into_args_tuple() {
let user = TypeName::importable("./models", "User");
let args: Vec<Arg> = (user, "hello").into_args();
assert_eq!(args.len(), 2);
assert!(matches!(&args[0], Arg::TypeName(_)));
assert!(matches!(&args[1], Arg::Literal(s) if s == "hello"));
}
#[test]
fn test_into_args_single_typename() {
let user = TypeName::importable("./models", "User");
let args: Vec<Arg> = user.into_args();
assert_eq!(args.len(), 1);
}
#[test]
fn test_into_args_single_str() {
let args: Vec<Arg> = "hello".into_args();
assert_eq!(args.len(), 1);
assert!(matches!(&args[0], Arg::Literal(s) if s == "hello"));
}
#[test]
fn test_raw_literal_rejects_unresolved_indent_marker() {
let result = CodeBlock::of("%L", "%>");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("unresolved indentation marker '%>'"));
assert!(err_msg.contains("CodeBlock/CodeFragment"));
}
#[test]
fn test_raw_literal_rejects_unresolved_dedent_marker() {
let result = CodeBlock::of("%L", "%<");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("unresolved indentation marker '%<'"));
}
#[test]
fn test_fragment_composes_indent_markers_structurally() {
let fragment = CodeFragment::of("%>nested%<", ()).unwrap();
let mut b = CodeBlock::builder();
b.add("outer\n", ());
b.add_fragment(fragment);
let block = b.build().unwrap();
let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
assert_eq!(output, "outer\n nested");
}
#[test]
fn test_fragment_rejects_unbalanced_indent_marker() {
let result = CodeFragment::of("%>nested", ());
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("unbalanced control flow"));
assert!(err_msg.contains("indent depth is 1"));
}
#[test]
fn test_fragment_rejects_unmatched_dedent_marker() {
let result = CodeFragment::of("%<nested", ());
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("unbalanced control flow"));
assert!(err_msg.contains("indent depth is -1"));
}
#[test]
fn test_builder_allows_incremental_balanced_indent_markers() {
let mut b = CodeBlock::builder();
b.add("outer\n", ());
b.add("%>", ());
b.add("nested", ());
b.add("%<", ());
let block = b.build().unwrap();
let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
assert_eq!(output, "outer\n nested");
}
#[test]
fn test_builder_rejects_unbalanced_parsed_indent_marker_at_build() {
let mut b = CodeBlock::builder();
b.add("%>", ());
let result = b.build();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("unbalanced control flow"));
assert!(err_msg.contains("indent depth is 1"));
}
#[test]
fn test_fragment_can_be_passed_to_percent_l() {
let fragment = CodeFragment::of("%>nested%<", ()).unwrap();
let block = CodeBlock::of("outer\n%L", fragment).unwrap();
let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
assert_eq!(output, "outer\n nested");
}
#[test]
fn test_fragment_preserves_imports_when_passed_to_percent_l() {
let user = TypeName::importable_type("./models", "User");
let fragment = CodeFragment::of("const user: %T = loadUser()", (user,)).unwrap();
let block = CodeBlock::of("%L", fragment).unwrap();
let imports = crate::import_collector::collect_imports(&block);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].module, "./models");
assert_eq!(imports[0].name, "User");
assert!(imports[0].is_type_only);
}
#[test]
fn test_fragment_accepts_nested_codeblock_arguments() {
let inner = CodeBlock::of("compute()", ()).unwrap();
let fragment = CodeFragment::of("return %L", inner).unwrap();
let block = CodeBlock::of("%L", fragment).unwrap();
let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
assert_eq!(output, "return compute()");
}
#[test]
fn test_ordinary_percent_text_stays_raw() {
let block = CodeBlock::of("progress = %L", "100%").unwrap();
let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
assert_eq!(output, "progress = 100%");
}
#[test]
fn test_collect_imports_from_codeblock() {
let user = TypeName::importable("./models", "User");
let tag = TypeName::importable("./models", "Tag");
let mut b = CodeBlock::builder();
b.add_statement("const u: %T = getUser()", (user,));
b.add_statement("const t: %T = getTag()", (tag,));
let block = b.build().unwrap();
let mut imports = Vec::new();
block.collect_imports(&mut imports);
assert_eq!(imports.len(), 2);
assert_eq!(imports[0].name, "User");
assert_eq!(imports[1].name, "Tag");
}
#[test]
fn test_nested_codeblock_imports() {
let user = TypeName::importable("./models", "User");
let mut ib = CodeBlock::builder();
ib.add_statement("return new %T()", (user,));
let inner = ib.build().unwrap();
let mut ob = CodeBlock::builder();
ob.add_code(inner);
let outer = ob.build().unwrap();
let mut imports = Vec::new();
outer.collect_imports(&mut imports);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].name, "User");
}
#[test]
fn test_name_arg() {
let mut b = CodeBlock::builder();
b.add("this.%N()", (NameArg("getUser".to_string()),));
let block = b.build().unwrap();
let has_name = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::NameRef(s) if s == "getUser"));
assert!(has_name);
}
#[test]
fn test_string_lit_arg() {
let mut b = CodeBlock::builder();
b.add("const x = %S", (StringLitArg("hello".to_string()),));
let block = b.build().unwrap();
let has_str_lit = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::StringLit(s) if s == "hello"));
assert!(has_str_lit);
}
#[test]
fn test_invalid_format_specifier() {
let mut b = CodeBlock::builder();
b.add("hello %X world", ());
let result = b.build();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("invalid format specifier"));
assert!(err_msg.contains("%X"));
}
#[test]
fn test_parse_format_invalid_specifier_returns_error() {
let result = parse_format("foo %Z bar");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("invalid format specifier"));
assert!(err_msg.contains("%Z"));
}
#[test]
fn test_mismatched_arg_count_includes_specifiers_and_kinds() {
let user = TypeName::importable("./models", "User");
let mut b = CodeBlock::builder();
b.add("%T %S %L", (user,));
let result = b.build();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("expects 3 args but got 1"));
assert!(err_msg.contains("%T"));
assert!(err_msg.contains("%S"));
assert!(err_msg.contains("%L"));
assert!(err_msg.contains("TypeName"));
}
#[test]
fn test_begin_control_flow_stores_condition() {
let mut b = CodeBlock::builder();
b.begin_control_flow("class Functor f", ());
b.add_statement("fmap :: (a -> b) -> f a -> f b", ());
b.end_control_flow();
let block = b.build().unwrap();
let has_open = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "class Functor f"));
assert!(has_open, "should contain BlockOpen with condition text");
let has_close = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::BlockClose(s) if s == "class Functor f"));
assert!(has_close, "should contain BlockClose with condition text");
}
#[test]
fn test_begin_control_flow_match_empty_open() {
let mut b = CodeBlock::builder();
b.begin_control_flow("match x with", ());
b.add("| Red -> red", ());
b.add_line();
b.end_control_flow();
let block = b.build().unwrap();
let has_open = block
.nodes
.iter()
.any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "match x with"));
assert!(has_open, "should contain BlockOpen(\"match x with\")");
}
}