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    /// End a control flow block with a trailing semicolon (for expression-level
383    /// control flow like `match` in PHP/Rust).
384    pub fn end_control_flow_with_semicolon(&mut self) -> &mut Self {
385        let condition = self.block_stack.pop().unwrap_or_default();
386        self.nodes.push(CodeNode::Dedent);
387        self.indent_depth -= 1;
388        self.nodes.push(CodeNode::BlockClose(condition));
389        self.nodes.push(CodeNode::StatementEnd);
390        self.nodes.push(CodeNode::Newline);
391        self
392    }
393
394    /// Add a blank line.
395    pub fn add_line(&mut self) -> &mut Self {
396        self.nodes.push(CodeNode::Newline);
397        self
398    }
399
400    /// Add an inline comment.
401    pub fn add_comment(&mut self, text: &str) -> &mut Self {
402        self.nodes.push(CodeNode::Comment(text.to_string()));
403        self.nodes.push(CodeNode::Newline);
404        self
405    }
406
407    /// Add a language-aware attribute / annotation.
408    ///
409    /// Rendered with the language's annotation prefix and suffix
410    /// (Rust: `#[text]`, Java/Python: `@text`, C++: `[[text]]`).
411    pub fn add_attribute(&mut self, text: &str) -> &mut Self {
412        self.nodes.push(CodeNode::Attribute(text.to_string()));
413        self.nodes.push(CodeNode::Newline);
414        self
415    }
416
417    /// Add a nested CodeBlock inline.
418    pub fn add_code(&mut self, block: CodeBlock) -> &mut Self {
419        self.nodes.push(CodeNode::Nested(block));
420        self
421    }
422
423    /// Build the immutable CodeBlock.
424    ///
425    /// Returns an error if any format string had an argument count mismatch,
426    /// or if indent depth is not balanced (unmatched
427    /// begin_control_flow / end_control_flow).
428    pub fn build(self) -> Result<CodeBlock, crate::error::SigilStitchError> {
429        if let Some(err) = self.errors.into_iter().next() {
430            return Err(err);
431        }
432        if self.indent_depth != 0 {
433            return Err(crate::error::SigilStitchError::UnbalancedIndent {
434                depth: self.indent_depth,
435            });
436        }
437        Ok(CodeBlock { nodes: self.nodes })
438    }
439
440    /// Build the CodeBlock, panicking on error.
441    pub fn build_unwrap(self) -> CodeBlock {
442        self.build().unwrap()
443    }
444}
445
446impl Default for CodeBlockBuilder {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452/// Parse a format string into FormatParts.
453fn parse_format(format: &str) -> Result<Vec<FormatPart>, crate::error::SigilStitchError> {
454    let mut parts = Vec::new();
455    let mut current_literal = String::new();
456    let mut chars = format.char_indices().peekable();
457
458    while let Some(&(_, ch)) = chars.peek() {
459        if ch == '%' {
460            chars.next();
461            if let Some(&(_, spec)) = chars.peek() {
462                chars.next();
463                let part = match spec {
464                    'W' => Some(FormatPart::Wrap),
465                    '>' => Some(FormatPart::Indent),
466                    '<' => Some(FormatPart::Dedent),
467                    '[' => Some(FormatPart::StatementBegin),
468                    ']' => Some(FormatPart::StatementEnd),
469                    '%' => {
470                        current_literal.push('%');
471                        continue;
472                    }
473                    _ => match Specifier::from_format_char(spec) {
474                        Some(s) => Some(FormatPart::Arg(s)),
475                        None => {
476                            return Err(crate::error::SigilStitchError::InvalidFormatSpecifier {
477                                format: format.to_string(),
478                                specifier: spec,
479                            });
480                        }
481                    },
482                };
483                if let Some(part) = part {
484                    if !current_literal.is_empty() {
485                        parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
486                    }
487                    parts.push(part);
488                }
489            }
490        } else if ch == '\n' {
491            chars.next();
492            if !current_literal.is_empty() {
493                parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
494            }
495            parts.push(FormatPart::Newline);
496        } else {
497            chars.next();
498            current_literal.push(ch);
499        }
500    }
501
502    if !current_literal.is_empty() {
503        parts.push(FormatPart::Literal(current_literal));
504    }
505
506    Ok(parts)
507}
508
509// === IntoArgs trait and implementations ===
510
511/// Trait for converting various types into a `Vec<Arg>` for format strings.
512///
513/// Implemented for `()` (no args), `TypeName`, `&str`, `String`, `CodeBlock`,
514/// `NameArg`, `StringLitArg`, `Vec<Arg>`, and tuples up to 8 elements.
515/// Bare strings convert to `Arg::Literal`; use [`NameArg`] or [`StringLitArg`]
516/// wrappers to target `%N` or `%S` specifiers instead.
517pub trait IntoArgs {
518    /// Convert into a vector of format arguments.
519    fn into_args(self) -> Vec<Arg>;
520}
521
522/// Empty args (for format strings with no specifiers).
523impl IntoArgs for () {
524    fn into_args(self) -> Vec<Arg> {
525        Vec::new()
526    }
527}
528
529/// Single TypeName arg.
530impl IntoArgs for TypeName {
531    fn into_args(self) -> Vec<Arg> {
532        vec![Arg::TypeName(self)]
533    }
534}
535
536/// Single string arg (as literal).
537impl IntoArgs for &str {
538    fn into_args(self) -> Vec<Arg> {
539        vec![Arg::Literal(self.to_string())]
540    }
541}
542
543impl IntoArgs for String {
544    fn into_args(self) -> Vec<Arg> {
545        vec![Arg::Literal(self)]
546    }
547}
548
549/// Single CodeBlock arg.
550impl IntoArgs for CodeBlock {
551    fn into_args(self) -> Vec<Arg> {
552        vec![Arg::Code(self)]
553    }
554}
555
556/// Pre-built args vector (used by specs that dynamically build format strings).
557impl IntoArgs for Vec<Arg> {
558    fn into_args(self) -> Vec<Arg> {
559        self
560    }
561}
562
563/// A wrapper to explicitly mark a string as a Name arg (for `%N`).
564///
565/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
566/// `NameArg` when your format string uses `%N`.
567///
568/// # Examples
569///
570/// ```
571/// use sigil_stitch::code_block::{CodeBlock, NameArg};
572/// use sigil_stitch::lang::typescript::TypeScript;
573///
574/// let mut cb = CodeBlock::builder();
575/// cb.add("this.%N()", (NameArg("getData".to_string()),));
576/// let block = cb.build().unwrap();
577/// ```
578pub struct NameArg(pub String);
579
580impl IntoArgs for NameArg {
581    fn into_args(self) -> Vec<Arg> {
582        vec![Arg::Name(self.0)]
583    }
584}
585
586/// A wrapper to explicitly mark a string as a StringLit arg (for `%S`).
587///
588/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
589/// `StringLitArg` when your format string uses `%S` to emit a quoted string.
590///
591/// # Examples
592///
593/// ```
594/// use sigil_stitch::code_block::{CodeBlock, StringLitArg};
595/// use sigil_stitch::lang::typescript::TypeScript;
596///
597/// let mut cb = CodeBlock::builder();
598/// cb.add_statement("const msg = %S", (StringLitArg("hello".to_string()),));
599/// let block = cb.build().unwrap();
600/// ```
601pub struct StringLitArg(pub String);
602
603impl IntoArgs for StringLitArg {
604    fn into_args(self) -> Vec<Arg> {
605        vec![Arg::StringLit(self.0)]
606    }
607}
608
609/// Wrapper for verbatim string literal arguments — preserves interpolation sigils.
610///
611/// Use `VerbatimStrArg` when your format string uses `%V` to emit a string with
612/// minimal escaping (only structural delimiters escaped, interpolation preserved).
613///
614/// ```ignore
615/// use sigil_stitch::code_block::{CodeBlock, VerbatimStrArg};
616///
617/// let mut cb = CodeBlock::builder();
618/// cb.add_statement("echo %V", (VerbatimStrArg("$HOME/.config".to_string()),));
619/// let block = cb.build().unwrap();
620/// ```
621pub struct VerbatimStrArg(pub String);
622
623impl IntoArgs for VerbatimStrArg {
624    fn into_args(self) -> Vec<Arg> {
625        vec![Arg::VerbatimStr(self.0)]
626    }
627}
628
629/// A wrapper to mark a string as an inline comment arg (for `%R` / `$comment`).
630///
631/// Use `CommentArg` when your format string uses `%R` to emit a language-specific
632/// comment at the current position.
633///
634/// # Examples
635///
636/// ```
637/// use sigil_stitch::code_block::{CodeBlock, CommentArg};
638///
639/// let mut cb = CodeBlock::builder();
640/// cb.add_statement("const x = 42; %R", (CommentArg("TODO: validate".to_string()),));
641/// let block = cb.build().unwrap();
642/// ```
643pub struct CommentArg(pub String);
644
645impl IntoArgs for CommentArg {
646    fn into_args(self) -> Vec<Arg> {
647        vec![Arg::Comment(self.0)]
648    }
649}
650
651// Individual Arg conversions.
652impl From<TypeName> for Arg {
653    fn from(tn: TypeName) -> Self {
654        Arg::TypeName(tn)
655    }
656}
657
658impl From<&str> for Arg {
659    fn from(s: &str) -> Self {
660        Arg::Literal(s.to_string())
661    }
662}
663
664impl From<String> for Arg {
665    fn from(s: String) -> Self {
666        Arg::Literal(s)
667    }
668}
669
670impl From<CodeBlock> for Arg {
671    fn from(cb: CodeBlock) -> Self {
672        Arg::Code(cb)
673    }
674}
675
676impl From<NameArg> for Arg {
677    fn from(n: NameArg) -> Self {
678        Arg::Name(n.0)
679    }
680}
681
682impl From<StringLitArg> for Arg {
683    fn from(s: StringLitArg) -> Self {
684        Arg::StringLit(s.0)
685    }
686}
687
688impl From<VerbatimStrArg> for Arg {
689    fn from(s: VerbatimStrArg) -> Self {
690        Arg::VerbatimStr(s.0)
691    }
692}
693
694impl From<CommentArg> for Arg {
695    fn from(s: CommentArg) -> Self {
696        Arg::Comment(s.0)
697    }
698}
699
700// Tuple implementations for IntoArgs.
701// Each element must implement Into<Arg>.
702
703macro_rules! impl_into_args_tuple {
704    ($($idx:tt $T:ident),+) => {
705        impl<$($T: Into<Arg>),+> IntoArgs for ($($T,)+) {
706            fn into_args(self) -> Vec<Arg> {
707                vec![$(self.$idx.into()),+]
708            }
709        }
710    };
711}
712
713impl_into_args_tuple!(0 A);
714impl_into_args_tuple!(0 A, 1 B);
715impl_into_args_tuple!(0 A, 1 B, 2 C);
716impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D);
717impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
718impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
719impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
720impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725    use crate::code_node::CodeNode;
726
727    #[test]
728    fn test_parse_all_specifiers() {
729        let parts = parse_format("hello %T world %N %S %L %W %> %< %[ %]").unwrap();
730        assert!(parts.contains(&FormatPart::Arg(Specifier::Type)));
731        assert!(parts.contains(&FormatPart::Arg(Specifier::Name)));
732        assert!(parts.contains(&FormatPart::Arg(Specifier::StringLit)));
733        assert!(parts.contains(&FormatPart::Arg(Specifier::Literal)));
734        assert!(parts.contains(&FormatPart::Wrap));
735        assert!(parts.contains(&FormatPart::Indent));
736        assert!(parts.contains(&FormatPart::Dedent));
737        assert!(parts.contains(&FormatPart::StatementBegin));
738        assert!(parts.contains(&FormatPart::StatementEnd));
739    }
740
741    #[test]
742    fn test_parse_literal_percent() {
743        let parts = parse_format("100%%").unwrap();
744        assert_eq!(parts, vec![FormatPart::Literal("100%".to_string())]);
745    }
746
747    #[test]
748    fn test_parse_empty() {
749        let parts = parse_format("").unwrap();
750        assert!(parts.is_empty());
751    }
752
753    #[test]
754    fn test_parse_newlines() {
755        let parts = parse_format("line1\nline2").unwrap();
756        assert_eq!(
757            parts,
758            vec![
759                FormatPart::Literal("line1".to_string()),
760                FormatPart::Newline,
761                FormatPart::Literal("line2".to_string()),
762            ]
763        );
764    }
765
766    #[test]
767    fn test_builder_add_statement() {
768        let mut b = CodeBlock::builder();
769        b.add_statement("const x = %L", "42");
770        let block = b.build().unwrap();
771
772        assert!(!block.is_empty());
773        let has_stmt_begin = block
774            .nodes
775            .iter()
776            .any(|n| matches!(n, CodeNode::StatementBegin));
777        let has_stmt_end = block
778            .nodes
779            .iter()
780            .any(|n| matches!(n, CodeNode::StatementEnd));
781        assert!(has_stmt_begin);
782        assert!(has_stmt_end);
783    }
784
785    #[test]
786    fn test_builder_control_flow() {
787        let mut b = CodeBlock::builder();
788        b.begin_control_flow("if (x > 0)", ());
789        b.add_statement("return x", ());
790        b.end_control_flow();
791        let block = b.build().unwrap();
792
793        assert!(!block.is_empty());
794    }
795
796    #[test]
797    fn test_builder_unbalanced_control_flow() {
798        let mut b = CodeBlock::builder();
799        b.begin_control_flow("if (x)", ());
800        b.add_statement("y()", ());
801        // missing end_control_flow
802        let result = b.build();
803        assert!(result.is_err());
804        assert!(result.unwrap_err().to_string().contains("unbalanced"));
805    }
806
807    #[test]
808    fn test_mismatched_arg_count() {
809        let mut b = CodeBlock::builder();
810        b.add("%T", ());
811        let result = b.build();
812        assert!(result.is_err());
813        assert!(
814            result
815                .unwrap_err()
816                .to_string()
817                .contains("expects 1 args but got 0")
818        );
819    }
820
821    #[test]
822    fn test_into_args_tuple() {
823        let user = TypeName::importable("./models", "User");
824        let args: Vec<Arg> = (user, "hello").into_args();
825        assert_eq!(args.len(), 2);
826        assert!(matches!(&args[0], Arg::TypeName(_)));
827        assert!(matches!(&args[1], Arg::Literal(s) if s == "hello"));
828    }
829
830    #[test]
831    fn test_into_args_single_typename() {
832        let user = TypeName::importable("./models", "User");
833        let args: Vec<Arg> = user.into_args();
834        assert_eq!(args.len(), 1);
835    }
836
837    #[test]
838    fn test_into_args_single_str() {
839        let args: Vec<Arg> = "hello".into_args();
840        assert_eq!(args.len(), 1);
841        assert!(matches!(&args[0], Arg::Literal(s) if s == "hello"));
842    }
843
844    #[test]
845    fn test_collect_imports_from_codeblock() {
846        let user = TypeName::importable("./models", "User");
847        let tag = TypeName::importable("./models", "Tag");
848        let mut b = CodeBlock::builder();
849        b.add_statement("const u: %T = getUser()", (user,));
850        b.add_statement("const t: %T = getTag()", (tag,));
851        let block = b.build().unwrap();
852
853        let mut imports = Vec::new();
854        block.collect_imports(&mut imports);
855        assert_eq!(imports.len(), 2);
856        assert_eq!(imports[0].name, "User");
857        assert_eq!(imports[1].name, "Tag");
858    }
859
860    #[test]
861    fn test_nested_codeblock_imports() {
862        let user = TypeName::importable("./models", "User");
863        let mut ib = CodeBlock::builder();
864        ib.add_statement("return new %T()", (user,));
865        let inner = ib.build().unwrap();
866
867        let mut ob = CodeBlock::builder();
868        ob.add_code(inner);
869        let outer = ob.build().unwrap();
870
871        let mut imports = Vec::new();
872        outer.collect_imports(&mut imports);
873        assert_eq!(imports.len(), 1);
874        assert_eq!(imports[0].name, "User");
875    }
876
877    #[test]
878    fn test_name_arg() {
879        let mut b = CodeBlock::builder();
880        b.add("this.%N()", (NameArg("getUser".to_string()),));
881        let block = b.build().unwrap();
882        let has_name = block
883            .nodes
884            .iter()
885            .any(|n| matches!(n, CodeNode::NameRef(s) if s == "getUser"));
886        assert!(has_name);
887    }
888
889    #[test]
890    fn test_string_lit_arg() {
891        let mut b = CodeBlock::builder();
892        b.add("const x = %S", (StringLitArg("hello".to_string()),));
893        let block = b.build().unwrap();
894        let has_str_lit = block
895            .nodes
896            .iter()
897            .any(|n| matches!(n, CodeNode::StringLit(s) if s == "hello"));
898        assert!(has_str_lit);
899    }
900
901    #[test]
902    fn test_invalid_format_specifier() {
903        let mut b = CodeBlock::builder();
904        b.add("hello %X world", ());
905        let result = b.build();
906        assert!(result.is_err());
907        let err_msg = result.unwrap_err().to_string();
908        assert!(err_msg.contains("invalid format specifier"));
909        assert!(err_msg.contains("%X"));
910    }
911
912    #[test]
913    fn test_parse_format_invalid_specifier_returns_error() {
914        let result = parse_format("foo %Z bar");
915        assert!(result.is_err());
916        let err_msg = result.unwrap_err().to_string();
917        assert!(err_msg.contains("invalid format specifier"));
918        assert!(err_msg.contains("%Z"));
919    }
920
921    #[test]
922    fn test_mismatched_arg_count_includes_specifiers_and_kinds() {
923        let user = TypeName::importable("./models", "User");
924        let mut b = CodeBlock::builder();
925        b.add("%T %S %L", (user,));
926        let result = b.build();
927        assert!(result.is_err());
928        let err_msg = result.unwrap_err().to_string();
929        assert!(err_msg.contains("expects 3 args but got 1"));
930        assert!(err_msg.contains("%T"));
931        assert!(err_msg.contains("%S"));
932        assert!(err_msg.contains("%L"));
933        assert!(err_msg.contains("TypeName"));
934    }
935
936    #[test]
937    fn test_begin_control_flow_stores_condition() {
938        let mut b = CodeBlock::builder();
939        b.begin_control_flow("class Functor f", ());
940        b.add_statement("fmap :: (a -> b) -> f a -> f b", ());
941        b.end_control_flow();
942        let block = b.build().unwrap();
943        let has_open = block
944            .nodes
945            .iter()
946            .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "class Functor f"));
947        assert!(has_open, "should contain BlockOpen with condition text");
948        let has_close = block
949            .nodes
950            .iter()
951            .any(|n| matches!(n, CodeNode::BlockClose(s) if s == "class Functor f"));
952        assert!(has_close, "should contain BlockClose with condition text");
953    }
954
955    #[test]
956    fn test_begin_control_flow_match_empty_open() {
957        let mut b = CodeBlock::builder();
958        b.begin_control_flow("match x with", ());
959        b.add("| Red -> red", ());
960        b.add_line();
961        b.end_control_flow();
962        let block = b.build().unwrap();
963        let has_open = block
964            .nodes
965            .iter()
966            .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "match x with"));
967        assert!(has_open, "should contain BlockOpen(\"match x with\")");
968    }
969}