Skip to main content

sigil_stitch/
code_block.rs

1use crate::code_node::{CodeNode, parts_args_to_nodes};
2use crate::import::ImportRef;
3use crate::lang::CodeLang;
4use crate::type_name::TypeName;
5
6/// Argument-consuming format specifier kinds.
7///
8/// This is the single source of truth for what interpolation specifiers exist.
9/// Both the library's `parse_format()` and the `sigil_quote!` macro's codegen
10/// map to these same logical kinds. The macro crate cannot import this type
11/// (proc-macro dependency cycle), but the format characters are shared: the
12/// macro emits `%T`/`%N`/`%S`/`%L` strings that `parse_format` then parses
13/// via [`Specifier::from_format_char`].
14///
15/// Adding a variant here without handling it in `parse_format` and
16/// `parts_args_to_nodes` will cause exhaustiveness errors.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18pub enum Specifier {
19    /// `%T` / `$T` — type reference (consumes `Arg::TypeName`).
20    Type,
21    /// `%N` / `$N` — name identifier (consumes `Arg::Name`).
22    Name,
23    /// `%S` / `$S` — string literal (consumes `Arg::StringLit`).
24    StringLit,
25    /// `%V` / `$V` — verbatim string literal (consumes `Arg::VerbatimStr`).
26    /// Escapes only structural delimiters, preserving interpolation sigils.
27    VerbatimStr,
28    /// `%L` / `$L` / `$C` — literal value or nested code block (consumes `Arg::Literal` or `Arg::Code`).
29    Literal,
30    /// `%R` / `$comment` — inline comment (consumes `Arg::Comment`).
31    Comment,
32}
33
34impl Specifier {
35    /// Map a format-string character to a specifier.
36    ///
37    /// Returns `None` for characters that are not argument-consuming specifiers
38    /// (e.g. `W`, `>`, `<`, `[`, `]`, `%`).
39    pub fn from_format_char(ch: char) -> Option<Self> {
40        match ch {
41            'T' => Some(Self::Type),
42            'N' => Some(Self::Name),
43            'S' => Some(Self::StringLit),
44            'V' => Some(Self::VerbatimStr),
45            'L' => Some(Self::Literal),
46            'R' => Some(Self::Comment),
47            _ => None,
48        }
49    }
50
51    /// The format-string character for this specifier.
52    pub fn format_char(self) -> char {
53        match self {
54            Self::Type => 'T',
55            Self::Name => 'N',
56            Self::StringLit => 'S',
57            Self::VerbatimStr => 'V',
58            Self::Literal => 'L',
59            Self::Comment => 'R',
60        }
61    }
62
63    /// All defined specifier variants.
64    pub fn all() -> &'static [Self] {
65        &[
66            Self::Type,
67            Self::Name,
68            Self::StringLit,
69            Self::VerbatimStr,
70            Self::Literal,
71            Self::Comment,
72        ]
73    }
74}
75
76/// A parsed format specifier from a format string.
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
78pub(crate) enum FormatPart {
79    /// Literal text (no interpolation).
80    Literal(String),
81    /// An argument-consuming specifier (`%T`, `%N`, `%S`, `%L`).
82    Arg(Specifier),
83    /// `%W` - soft line break point (no argument consumed).
84    Wrap,
85    /// `%>` - increase indent (no argument consumed).
86    Indent,
87    /// `%<` - decrease indent (no argument consumed).
88    Dedent,
89    /// `%[` - statement begin (no argument consumed).
90    StatementBegin,
91    /// `%]` - statement end (no argument consumed).
92    StatementEnd,
93    /// Newline.
94    Newline,
95    /// Block open delimiter — resolved at render time via `lang.block_open_for(condition)`
96    /// falling back to `lang.block_syntax().block_open`. Carries the condition text
97    /// from `begin_control_flow` (e.g., `"if x > 0"`, `"for i in range(10)"`).
98    /// Empty string means no condition (e.g., a bare `{ }` block).
99    BlockOpen(String),
100    /// Terminal block close delimiter — resolved at render time via
101    /// `lang.block_close_for(condition)` falling back to `lang.block_syntax().block_close`.
102    /// Carries the condition from the matching `begin_control_flow`.
103    /// Emits: closer only.
104    BlockClose(String),
105    /// Non-terminal block close before a branch keyword (`else`, `elif`, `catch`).
106    /// Like `BlockClose` but emits closer + space (not newline) so the branch
107    /// keyword continues on the same line (e.g., `} else {`).
108    /// Suppressed when `block_syntax().close_on_transition` is `false`.
109    BranchClose(String),
110}
111
112/// An argument to a CodeBlock format string.
113#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
114pub enum Arg {
115    /// A type name reference (used by `%T`).
116    TypeName(TypeName),
117    /// A name string (used by `%N`).
118    Name(String),
119    /// A string literal value (used by `%S`).
120    StringLit(String),
121    /// A verbatim string literal value (used by `%V`).
122    VerbatimStr(String),
123    /// A literal string value or nested code block (used by `%L`).
124    Literal(String),
125    /// A nested code block (used by `%L`).
126    Code(CodeBlock),
127    /// An inline comment (used by `%R` / `$comment`).
128    Comment(String),
129}
130
131/// An immutable code fragment with embedded type references.
132///
133/// `CodeBlock` is the core composition primitive in sigil-stitch. It stores a tree
134/// of [`CodeNode`] nodes — self-contained IR nodes produced from format strings
135/// (`%T`, `%N`, `%S`, `%L`, etc.). CodeBlocks are produced by [`CodeBlockBuilder`]
136/// and consumed by [`FileSpec`](crate::spec::file_spec::FileSpec) during rendering.
137/// Type references embedded via `%T` are automatically tracked for import resolution.
138///
139/// Use [`CodeBlock::builder()`] to construct a block incrementally, or
140/// [`CodeBlock::of()`] for simple one-liners.
141///
142/// # Examples
143///
144/// ```
145/// use sigil_stitch::code_block::CodeBlock;
146/// use sigil_stitch::lang::typescript::TypeScript;
147/// use sigil_stitch::type_name::TypeName;
148///
149/// // One-liner with a type reference:
150/// let user = TypeName::importable("./models", "User");
151/// let block = CodeBlock::of("const u: %T = getUser()", (user,)).unwrap();
152///
153/// // Multi-statement block via builder:
154/// let mut cb = CodeBlock::builder();
155/// cb.add_statement("const x = 1", ());
156/// cb.add_statement("const y = 2", ());
157/// let block = cb.build().unwrap();
158/// ```
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160pub struct CodeBlock {
161    pub(crate) nodes: Vec<CodeNode>,
162}
163
164impl CodeBlock {
165    /// Create a new CodeBlockBuilder.
166    pub fn builder() -> CodeBlockBuilder {
167        CodeBlockBuilder::new()
168    }
169
170    /// Access the node tree for rewriting. Used by language rewrite passes.
171    pub fn nodes_mut(&mut self) -> &mut Vec<CodeNode> {
172        &mut self.nodes
173    }
174
175    /// Create a CodeBlock from a single format string and arguments.
176    pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
177        let mut builder = CodeBlockBuilder::new();
178        builder.add(format, args);
179        builder.build()
180    }
181
182    /// Check if this code block is empty.
183    pub fn is_empty(&self) -> bool {
184        self.nodes.is_empty()
185    }
186
187    /// Check if this code block ends with a newline or block close.
188    pub fn ends_with_newline_or_block_close(&self) -> bool {
189        fn check_last(nodes: &[CodeNode]) -> bool {
190            match nodes.last() {
191                Some(CodeNode::Newline | CodeNode::BlockClose(_)) => true,
192                Some(CodeNode::Sequence(children)) => check_last(children),
193                Some(CodeNode::Nested(inner)) => check_last(&inner.nodes),
194                _ => false,
195            }
196        }
197        check_last(&self.nodes)
198    }
199
200    /// Collect all import references from this code block.
201    pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
202        crate::import_collector::walk_nodes(&self.nodes, out);
203    }
204
205    /// Render this code block to a string without import resolution.
206    ///
207    /// Creates a temporary empty import group and renders using the given
208    /// language and target line width. Useful for quick one-off rendering
209    /// in tests or when import management is not needed.
210    pub fn render_standalone(
211        &self,
212        lang: &dyn CodeLang,
213        width: usize,
214    ) -> Result<String, crate::error::SigilStitchError> {
215        let imports = crate::import::ImportGroup::new();
216        let mut renderer = crate::code_renderer::CodeRenderer::new(lang, &imports, width);
217        renderer.render(self)
218    }
219}
220
221/// Builder for constructing [`CodeBlock`] instances.
222///
223/// Provides methods for adding formatted code fragments, statements, control
224/// flow blocks, and nested code blocks. Format strings use `%T`, `%N`, `%S`,
225/// `%L` for type/name/string/literal substitution, and `%W`, `%>`, `%<` for
226/// soft line breaks and indentation.
227///
228/// # Examples
229///
230/// ```
231/// use sigil_stitch::code_block::CodeBlock;
232/// use sigil_stitch::lang::typescript::TypeScript;
233///
234/// let mut cb = CodeBlock::builder();
235/// cb.begin_control_flow("if (x > 0)", ());
236/// cb.add_statement("return x", ());
237/// cb.next_control_flow("else", ());
238/// cb.add_statement("return -x", ());
239/// cb.end_control_flow();
240/// let block = cb.build().unwrap();
241/// ```
242#[derive(Debug)]
243pub struct CodeBlockBuilder {
244    nodes: Vec<CodeNode>,
245    indent_depth: i32,
246    block_stack: Vec<String>,
247    errors: Vec<crate::error::SigilStitchError>,
248}
249
250impl CodeBlockBuilder {
251    /// Create a new empty code block builder.
252    pub fn new() -> Self {
253        Self {
254            nodes: Vec::new(),
255            indent_depth: 0,
256            block_stack: Vec::new(),
257            errors: Vec::new(),
258        }
259    }
260
261    /// Add a formatted code fragment.
262    pub fn add(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
263        let arg_vec = args.into_args();
264        let parsed = match parse_format(format) {
265            Ok(parts) => parts,
266            Err(err) => {
267                self.errors.push(err);
268                return self;
269            }
270        };
271
272        let consuming_specifiers: Vec<String> = parsed
273            .iter()
274            .filter_map(|p| match p {
275                FormatPart::Arg(s) => Some(format!("%{}", s.format_char())),
276                _ => None,
277            })
278            .collect();
279
280        let expected_args = consuming_specifiers.len();
281
282        if expected_args != arg_vec.len() {
283            let actual_arg_kinds: Vec<String> = arg_vec
284                .iter()
285                .map(|a| match a {
286                    Arg::TypeName(_) => "TypeName".to_string(),
287                    Arg::Name(_) => "Name".to_string(),
288                    Arg::StringLit(_) => "StringLit".to_string(),
289                    Arg::VerbatimStr(_) => "VerbatimStr".to_string(),
290                    Arg::Literal(_) => "Literal".to_string(),
291                    Arg::Code(_) => "Code".to_string(),
292                    Arg::Comment(_) => "Comment".to_string(),
293                })
294                .collect();
295            self.errors
296                .push(crate::error::SigilStitchError::FormatArgCount {
297                    format: format.to_string(),
298                    expected: expected_args,
299                    actual: arg_vec.len(),
300                    expected_specifiers: consuming_specifiers,
301                    actual_arg_kinds,
302                });
303            return self;
304        }
305
306        let new_nodes = parts_args_to_nodes(&parsed, &arg_vec);
307        self.nodes.extend(new_nodes);
308        self
309    }
310
311    /// Add a statement (wraps in %[...%] and appends language semicolon).
312    pub fn add_statement(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
313        self.nodes.push(CodeNode::StatementBegin);
314        self.add(format, args);
315        self.nodes.push(CodeNode::StatementEnd);
316        self.nodes.push(CodeNode::Newline);
317        self
318    }
319
320    /// Begin a control flow block (e.g., "if foo" -> "if foo {\n" + indent).
321    ///
322    /// The **raw format string** (not the interpolated result) is stored as
323    /// the condition text and passed to `block_open_for` / `block_close_for`
324    /// at render time, enabling language backends to emit context-aware
325    /// delimiters (e.g., Bash `then`/`fi` for `if`, `do`/`done` for `for`).
326    ///
327    /// Because backends pattern-match on the stored condition (e.g.,
328    /// `condition.starts_with("if ")`), avoid interpolating into the keyword
329    /// prefix — `begin_control_flow("if %L", expr)` works, but
330    /// `begin_control_flow("%L x", some_keyword)` would not be recognized.
331    pub fn begin_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
332        let condition = format.to_string();
333        self.block_stack.push(condition.clone());
334        self.add(format, args);
335        self.nodes.push(CodeNode::BlockOpen(condition));
336        self.nodes.push(CodeNode::Newline);
337        self.nodes.push(CodeNode::Indent);
338        self.indent_depth += 1;
339        self
340    }
341
342    /// Add an else/else-if clause (e.g., "} else {" or "elif ...:" for Python).
343    pub fn next_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
344        let condition = self.block_stack.last().cloned().unwrap_or_default();
345        self.nodes.push(CodeNode::Dedent);
346        self.indent_depth -= 1;
347        self.nodes.push(CodeNode::BranchClose(condition));
348        self.add(format, args);
349        let new_condition = format.to_string();
350        self.nodes.push(CodeNode::BlockOpen(new_condition));
351        self.nodes.push(CodeNode::Newline);
352        self.nodes.push(CodeNode::Indent);
353        self.indent_depth += 1;
354        self
355    }
356
357    /// End a control flow block (emits language-specific closer + newline,
358    /// decreases indent).
359    pub fn end_control_flow(&mut self) -> &mut Self {
360        let condition = self.block_stack.pop().unwrap_or_default();
361        self.nodes.push(CodeNode::Dedent);
362        self.indent_depth -= 1;
363        self.nodes.push(CodeNode::BlockClose(condition));
364        self.nodes.push(CodeNode::Newline);
365        self
366    }
367
368    /// End a control flow block without a trailing newline.
369    ///
370    /// Used when the block is nested inside a `Statement::Statement` via
371    /// `%L` (e.g., expression braces in format strings). The outer
372    /// `add_statement` provides both `;` via `StatementEnd` and `\n` via
373    /// `Newline`.
374    pub fn end_control_flow_no_newline(&mut self) -> &mut Self {
375        let condition = self.block_stack.pop().unwrap_or_default();
376        self.nodes.push(CodeNode::Dedent);
377        self.indent_depth -= 1;
378        self.nodes.push(CodeNode::BlockClose(condition));
379        self
380    }
381
382    /// Add a blank line.
383    pub fn add_line(&mut self) -> &mut Self {
384        self.nodes.push(CodeNode::Newline);
385        self
386    }
387
388    /// Add an inline comment.
389    pub fn add_comment(&mut self, text: &str) -> &mut Self {
390        self.nodes.push(CodeNode::Comment(text.to_string()));
391        self.nodes.push(CodeNode::Newline);
392        self
393    }
394
395    /// Add a language-aware attribute / annotation.
396    ///
397    /// Rendered with the language's annotation prefix and suffix
398    /// (Rust: `#[text]`, Java/Python: `@text`, C++: `[[text]]`).
399    pub fn add_attribute(&mut self, text: &str) -> &mut Self {
400        self.nodes.push(CodeNode::Attribute(text.to_string()));
401        self.nodes.push(CodeNode::Newline);
402        self
403    }
404
405    /// Add a nested CodeBlock inline.
406    pub fn add_code(&mut self, block: CodeBlock) -> &mut Self {
407        self.nodes.push(CodeNode::Nested(block));
408        self
409    }
410
411    /// Build the immutable CodeBlock.
412    ///
413    /// Returns an error if any format string had an argument count mismatch,
414    /// or if indent depth is not balanced (unmatched
415    /// begin_control_flow / end_control_flow).
416    pub fn build(self) -> Result<CodeBlock, crate::error::SigilStitchError> {
417        if let Some(err) = self.errors.into_iter().next() {
418            return Err(err);
419        }
420        if self.indent_depth != 0 {
421            return Err(crate::error::SigilStitchError::UnbalancedIndent {
422                depth: self.indent_depth,
423            });
424        }
425        Ok(CodeBlock { nodes: self.nodes })
426    }
427
428    /// Build the CodeBlock, panicking on error.
429    pub fn build_unwrap(self) -> CodeBlock {
430        self.build().unwrap()
431    }
432}
433
434impl Default for CodeBlockBuilder {
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440/// Parse a format string into FormatParts.
441fn parse_format(format: &str) -> Result<Vec<FormatPart>, crate::error::SigilStitchError> {
442    let mut parts = Vec::new();
443    let mut current_literal = String::new();
444    let mut chars = format.char_indices().peekable();
445
446    while let Some(&(_, ch)) = chars.peek() {
447        if ch == '%' {
448            chars.next();
449            if let Some(&(_, spec)) = chars.peek() {
450                chars.next();
451                let part = match spec {
452                    'W' => Some(FormatPart::Wrap),
453                    '>' => Some(FormatPart::Indent),
454                    '<' => Some(FormatPart::Dedent),
455                    '[' => Some(FormatPart::StatementBegin),
456                    ']' => Some(FormatPart::StatementEnd),
457                    '%' => {
458                        current_literal.push('%');
459                        continue;
460                    }
461                    _ => match Specifier::from_format_char(spec) {
462                        Some(s) => Some(FormatPart::Arg(s)),
463                        None => {
464                            return Err(crate::error::SigilStitchError::InvalidFormatSpecifier {
465                                format: format.to_string(),
466                                specifier: spec,
467                            });
468                        }
469                    },
470                };
471                if let Some(part) = part {
472                    if !current_literal.is_empty() {
473                        parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
474                    }
475                    parts.push(part);
476                }
477            }
478        } else if ch == '\n' {
479            chars.next();
480            if !current_literal.is_empty() {
481                parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
482            }
483            parts.push(FormatPart::Newline);
484        } else {
485            chars.next();
486            current_literal.push(ch);
487        }
488    }
489
490    if !current_literal.is_empty() {
491        parts.push(FormatPart::Literal(current_literal));
492    }
493
494    Ok(parts)
495}
496
497// === IntoArgs trait and implementations ===
498
499/// Trait for converting various types into a `Vec<Arg>` for format strings.
500///
501/// Implemented for `()` (no args), `TypeName`, `&str`, `String`, `CodeBlock`,
502/// `NameArg`, `StringLitArg`, `Vec<Arg>`, and tuples up to 8 elements.
503/// Bare strings convert to `Arg::Literal`; use [`NameArg`] or [`StringLitArg`]
504/// wrappers to target `%N` or `%S` specifiers instead.
505pub trait IntoArgs {
506    /// Convert into a vector of format arguments.
507    fn into_args(self) -> Vec<Arg>;
508}
509
510/// Empty args (for format strings with no specifiers).
511impl IntoArgs for () {
512    fn into_args(self) -> Vec<Arg> {
513        Vec::new()
514    }
515}
516
517/// Single TypeName arg.
518impl IntoArgs for TypeName {
519    fn into_args(self) -> Vec<Arg> {
520        vec![Arg::TypeName(self)]
521    }
522}
523
524/// Single string arg (as literal).
525impl IntoArgs for &str {
526    fn into_args(self) -> Vec<Arg> {
527        vec![Arg::Literal(self.to_string())]
528    }
529}
530
531impl IntoArgs for String {
532    fn into_args(self) -> Vec<Arg> {
533        vec![Arg::Literal(self)]
534    }
535}
536
537/// Single CodeBlock arg.
538impl IntoArgs for CodeBlock {
539    fn into_args(self) -> Vec<Arg> {
540        vec![Arg::Code(self)]
541    }
542}
543
544/// Pre-built args vector (used by specs that dynamically build format strings).
545impl IntoArgs for Vec<Arg> {
546    fn into_args(self) -> Vec<Arg> {
547        self
548    }
549}
550
551/// A wrapper to explicitly mark a string as a Name arg (for `%N`).
552///
553/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
554/// `NameArg` when your format string uses `%N`.
555///
556/// # Examples
557///
558/// ```
559/// use sigil_stitch::code_block::{CodeBlock, NameArg};
560/// use sigil_stitch::lang::typescript::TypeScript;
561///
562/// let mut cb = CodeBlock::builder();
563/// cb.add("this.%N()", (NameArg("getData".to_string()),));
564/// let block = cb.build().unwrap();
565/// ```
566pub struct NameArg(pub String);
567
568impl IntoArgs for NameArg {
569    fn into_args(self) -> Vec<Arg> {
570        vec![Arg::Name(self.0)]
571    }
572}
573
574/// A wrapper to explicitly mark a string as a StringLit arg (for `%S`).
575///
576/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
577/// `StringLitArg` when your format string uses `%S` to emit a quoted string.
578///
579/// # Examples
580///
581/// ```
582/// use sigil_stitch::code_block::{CodeBlock, StringLitArg};
583/// use sigil_stitch::lang::typescript::TypeScript;
584///
585/// let mut cb = CodeBlock::builder();
586/// cb.add_statement("const msg = %S", (StringLitArg("hello".to_string()),));
587/// let block = cb.build().unwrap();
588/// ```
589pub struct StringLitArg(pub String);
590
591impl IntoArgs for StringLitArg {
592    fn into_args(self) -> Vec<Arg> {
593        vec![Arg::StringLit(self.0)]
594    }
595}
596
597/// Wrapper for verbatim string literal arguments — preserves interpolation sigils.
598///
599/// Use `VerbatimStrArg` when your format string uses `%V` to emit a string with
600/// minimal escaping (only structural delimiters escaped, interpolation preserved).
601///
602/// ```ignore
603/// use sigil_stitch::code_block::{CodeBlock, VerbatimStrArg};
604///
605/// let mut cb = CodeBlock::builder();
606/// cb.add_statement("echo %V", (VerbatimStrArg("$HOME/.config".to_string()),));
607/// let block = cb.build().unwrap();
608/// ```
609pub struct VerbatimStrArg(pub String);
610
611impl IntoArgs for VerbatimStrArg {
612    fn into_args(self) -> Vec<Arg> {
613        vec![Arg::VerbatimStr(self.0)]
614    }
615}
616
617/// A wrapper to mark a string as an inline comment arg (for `%R` / `$comment`).
618///
619/// Use `CommentArg` when your format string uses `%R` to emit a language-specific
620/// comment at the current position.
621///
622/// # Examples
623///
624/// ```
625/// use sigil_stitch::code_block::{CodeBlock, CommentArg};
626///
627/// let mut cb = CodeBlock::builder();
628/// cb.add_statement("const x = 42; %R", (CommentArg("TODO: validate".to_string()),));
629/// let block = cb.build().unwrap();
630/// ```
631pub struct CommentArg(pub String);
632
633impl IntoArgs for CommentArg {
634    fn into_args(self) -> Vec<Arg> {
635        vec![Arg::Comment(self.0)]
636    }
637}
638
639// Individual Arg conversions.
640impl From<TypeName> for Arg {
641    fn from(tn: TypeName) -> Self {
642        Arg::TypeName(tn)
643    }
644}
645
646impl From<&str> for Arg {
647    fn from(s: &str) -> Self {
648        Arg::Literal(s.to_string())
649    }
650}
651
652impl From<String> for Arg {
653    fn from(s: String) -> Self {
654        Arg::Literal(s)
655    }
656}
657
658impl From<CodeBlock> for Arg {
659    fn from(cb: CodeBlock) -> Self {
660        Arg::Code(cb)
661    }
662}
663
664impl From<NameArg> for Arg {
665    fn from(n: NameArg) -> Self {
666        Arg::Name(n.0)
667    }
668}
669
670impl From<StringLitArg> for Arg {
671    fn from(s: StringLitArg) -> Self {
672        Arg::StringLit(s.0)
673    }
674}
675
676impl From<VerbatimStrArg> for Arg {
677    fn from(s: VerbatimStrArg) -> Self {
678        Arg::VerbatimStr(s.0)
679    }
680}
681
682impl From<CommentArg> for Arg {
683    fn from(s: CommentArg) -> Self {
684        Arg::Comment(s.0)
685    }
686}
687
688// Tuple implementations for IntoArgs.
689// Each element must implement Into<Arg>.
690
691macro_rules! impl_into_args_tuple {
692    ($($idx:tt $T:ident),+) => {
693        impl<$($T: Into<Arg>),+> IntoArgs for ($($T,)+) {
694            fn into_args(self) -> Vec<Arg> {
695                vec![$(self.$idx.into()),+]
696            }
697        }
698    };
699}
700
701impl_into_args_tuple!(0 A);
702impl_into_args_tuple!(0 A, 1 B);
703impl_into_args_tuple!(0 A, 1 B, 2 C);
704impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D);
705impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
706impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
707impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
708impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use crate::code_node::CodeNode;
714
715    #[test]
716    fn test_parse_all_specifiers() {
717        let parts = parse_format("hello %T world %N %S %L %W %> %< %[ %]").unwrap();
718        assert!(parts.contains(&FormatPart::Arg(Specifier::Type)));
719        assert!(parts.contains(&FormatPart::Arg(Specifier::Name)));
720        assert!(parts.contains(&FormatPart::Arg(Specifier::StringLit)));
721        assert!(parts.contains(&FormatPart::Arg(Specifier::Literal)));
722        assert!(parts.contains(&FormatPart::Wrap));
723        assert!(parts.contains(&FormatPart::Indent));
724        assert!(parts.contains(&FormatPart::Dedent));
725        assert!(parts.contains(&FormatPart::StatementBegin));
726        assert!(parts.contains(&FormatPart::StatementEnd));
727    }
728
729    #[test]
730    fn test_parse_literal_percent() {
731        let parts = parse_format("100%%").unwrap();
732        assert_eq!(parts, vec![FormatPart::Literal("100%".to_string())]);
733    }
734
735    #[test]
736    fn test_parse_empty() {
737        let parts = parse_format("").unwrap();
738        assert!(parts.is_empty());
739    }
740
741    #[test]
742    fn test_parse_newlines() {
743        let parts = parse_format("line1\nline2").unwrap();
744        assert_eq!(
745            parts,
746            vec![
747                FormatPart::Literal("line1".to_string()),
748                FormatPart::Newline,
749                FormatPart::Literal("line2".to_string()),
750            ]
751        );
752    }
753
754    #[test]
755    fn test_builder_add_statement() {
756        let mut b = CodeBlock::builder();
757        b.add_statement("const x = %L", "42");
758        let block = b.build().unwrap();
759
760        assert!(!block.is_empty());
761        let has_stmt_begin = block
762            .nodes
763            .iter()
764            .any(|n| matches!(n, CodeNode::StatementBegin));
765        let has_stmt_end = block
766            .nodes
767            .iter()
768            .any(|n| matches!(n, CodeNode::StatementEnd));
769        assert!(has_stmt_begin);
770        assert!(has_stmt_end);
771    }
772
773    #[test]
774    fn test_builder_control_flow() {
775        let mut b = CodeBlock::builder();
776        b.begin_control_flow("if (x > 0)", ());
777        b.add_statement("return x", ());
778        b.end_control_flow();
779        let block = b.build().unwrap();
780
781        assert!(!block.is_empty());
782    }
783
784    #[test]
785    fn test_builder_unbalanced_control_flow() {
786        let mut b = CodeBlock::builder();
787        b.begin_control_flow("if (x)", ());
788        b.add_statement("y()", ());
789        // missing end_control_flow
790        let result = b.build();
791        assert!(result.is_err());
792        assert!(result.unwrap_err().to_string().contains("unbalanced"));
793    }
794
795    #[test]
796    fn test_mismatched_arg_count() {
797        let mut b = CodeBlock::builder();
798        b.add("%T", ());
799        let result = b.build();
800        assert!(result.is_err());
801        assert!(
802            result
803                .unwrap_err()
804                .to_string()
805                .contains("expects 1 args but got 0")
806        );
807    }
808
809    #[test]
810    fn test_into_args_tuple() {
811        let user = TypeName::importable("./models", "User");
812        let args: Vec<Arg> = (user, "hello").into_args();
813        assert_eq!(args.len(), 2);
814        assert!(matches!(&args[0], Arg::TypeName(_)));
815        assert!(matches!(&args[1], Arg::Literal(s) if s == "hello"));
816    }
817
818    #[test]
819    fn test_into_args_single_typename() {
820        let user = TypeName::importable("./models", "User");
821        let args: Vec<Arg> = user.into_args();
822        assert_eq!(args.len(), 1);
823    }
824
825    #[test]
826    fn test_into_args_single_str() {
827        let args: Vec<Arg> = "hello".into_args();
828        assert_eq!(args.len(), 1);
829        assert!(matches!(&args[0], Arg::Literal(s) if s == "hello"));
830    }
831
832    #[test]
833    fn test_collect_imports_from_codeblock() {
834        let user = TypeName::importable("./models", "User");
835        let tag = TypeName::importable("./models", "Tag");
836        let mut b = CodeBlock::builder();
837        b.add_statement("const u: %T = getUser()", (user,));
838        b.add_statement("const t: %T = getTag()", (tag,));
839        let block = b.build().unwrap();
840
841        let mut imports = Vec::new();
842        block.collect_imports(&mut imports);
843        assert_eq!(imports.len(), 2);
844        assert_eq!(imports[0].name, "User");
845        assert_eq!(imports[1].name, "Tag");
846    }
847
848    #[test]
849    fn test_nested_codeblock_imports() {
850        let user = TypeName::importable("./models", "User");
851        let mut ib = CodeBlock::builder();
852        ib.add_statement("return new %T()", (user,));
853        let inner = ib.build().unwrap();
854
855        let mut ob = CodeBlock::builder();
856        ob.add_code(inner);
857        let outer = ob.build().unwrap();
858
859        let mut imports = Vec::new();
860        outer.collect_imports(&mut imports);
861        assert_eq!(imports.len(), 1);
862        assert_eq!(imports[0].name, "User");
863    }
864
865    #[test]
866    fn test_name_arg() {
867        let mut b = CodeBlock::builder();
868        b.add("this.%N()", (NameArg("getUser".to_string()),));
869        let block = b.build().unwrap();
870        let has_name = block
871            .nodes
872            .iter()
873            .any(|n| matches!(n, CodeNode::NameRef(s) if s == "getUser"));
874        assert!(has_name);
875    }
876
877    #[test]
878    fn test_string_lit_arg() {
879        let mut b = CodeBlock::builder();
880        b.add("const x = %S", (StringLitArg("hello".to_string()),));
881        let block = b.build().unwrap();
882        let has_str_lit = block
883            .nodes
884            .iter()
885            .any(|n| matches!(n, CodeNode::StringLit(s) if s == "hello"));
886        assert!(has_str_lit);
887    }
888
889    #[test]
890    fn test_invalid_format_specifier() {
891        let mut b = CodeBlock::builder();
892        b.add("hello %X world", ());
893        let result = b.build();
894        assert!(result.is_err());
895        let err_msg = result.unwrap_err().to_string();
896        assert!(err_msg.contains("invalid format specifier"));
897        assert!(err_msg.contains("%X"));
898    }
899
900    #[test]
901    fn test_parse_format_invalid_specifier_returns_error() {
902        let result = parse_format("foo %Z bar");
903        assert!(result.is_err());
904        let err_msg = result.unwrap_err().to_string();
905        assert!(err_msg.contains("invalid format specifier"));
906        assert!(err_msg.contains("%Z"));
907    }
908
909    #[test]
910    fn test_mismatched_arg_count_includes_specifiers_and_kinds() {
911        let user = TypeName::importable("./models", "User");
912        let mut b = CodeBlock::builder();
913        b.add("%T %S %L", (user,));
914        let result = b.build();
915        assert!(result.is_err());
916        let err_msg = result.unwrap_err().to_string();
917        assert!(err_msg.contains("expects 3 args but got 1"));
918        assert!(err_msg.contains("%T"));
919        assert!(err_msg.contains("%S"));
920        assert!(err_msg.contains("%L"));
921        assert!(err_msg.contains("TypeName"));
922    }
923
924    #[test]
925    fn test_begin_control_flow_stores_condition() {
926        let mut b = CodeBlock::builder();
927        b.begin_control_flow("class Functor f", ());
928        b.add_statement("fmap :: (a -> b) -> f a -> f b", ());
929        b.end_control_flow();
930        let block = b.build().unwrap();
931        let has_open = block
932            .nodes
933            .iter()
934            .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "class Functor f"));
935        assert!(has_open, "should contain BlockOpen with condition text");
936        let has_close = block
937            .nodes
938            .iter()
939            .any(|n| matches!(n, CodeNode::BlockClose(s) if s == "class Functor f"));
940        assert!(has_close, "should contain BlockClose with condition text");
941    }
942
943    #[test]
944    fn test_begin_control_flow_match_empty_open() {
945        let mut b = CodeBlock::builder();
946        b.begin_control_flow("match x with", ());
947        b.add("| Red -> red", ());
948        b.add_line();
949        b.end_control_flow();
950        let block = b.build().unwrap();
951        let has_open = block
952            .nodes
953            .iter()
954            .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "match x with"));
955        assert!(has_open, "should contain BlockOpen(\"match x with\")");
956    }
957}