Skip to main content

spikard_cli/codegen/formatters/
mod.rs

1//! Formatters for language-specific code generation output
2//!
3//! This module provides a trait-based system for formatting generated code in a way
4//! that respects language-specific conventions and tooling requirements. Each language
5//! binding (Python, TypeScript, Ruby, PHP) implements the `Formatter` trait to handle
6//! header formatting, import organization, docstring styling, and section merging.
7//!
8//! # Design
9//!
10//! The `Formatter` trait abstracts away language-specific formatting rules while ensuring
11//! consistent code generation across the entire spikard toolkit. Implementers handle:
12//!
13//! - **Headers**: Shebangs, auto-generation notices, module docstrings
14//! - **Imports**: Dependency declarations, type imports, organization
15//! - **Docstrings**: Language-specific documentation formatting (`NumPy`, `JSDoc`, etc.)
16//! - **Merging**: Combining sections with proper spacing and deduplication
17//!
18//! # Example
19//!
20//! ```no_run
21//! use spikard_cli::codegen::formatters::{Formatter, Import, HeaderMetadata, PythonFormatter, PhpFormatter, RustFormatter};
22//!
23//! let formatter = PythonFormatter::new();
24//! let metadata = HeaderMetadata {
25//!     auto_generated: true,
26//!     schema_file: Some("schema.graphql".to_string()),
27//!     generator_version: Some("0.6.2".to_string()),
28//! };
29//!
30//! let header = formatter.format_header(&metadata);
31//! println!("{}", header);
32//! ```
33
34mod php;
35mod python;
36mod ruby;
37mod rust_lang;
38mod typescript;
39
40pub use php::PhpFormatter;
41pub use python::PythonFormatter;
42pub use ruby::RubyFormatter;
43pub use rust_lang::RustFormatter;
44pub use typescript::TypeScriptFormatter;
45
46/// Metadata about a generated file used when formatting headers
47#[derive(Debug, Clone)]
48pub struct HeaderMetadata {
49    /// Whether this file is auto-generated and should not be edited manually
50    pub auto_generated: bool,
51    /// Optional path to the source schema file (GraphQL, `OpenAPI`, etc.)
52    pub schema_file: Option<String>,
53    /// Optional version of the generator tool that created this file
54    pub generator_version: Option<String>,
55}
56
57/// Represents an import/require/use statement in any language
58#[derive(Debug, Clone)]
59pub struct Import {
60    /// Module or package name (e.g., "typing", "graphql", "@apollo/client")
61    pub module: String,
62    /// Specific items to import (e.g., ["List", "Dict"] for Python)
63    /// If empty, import the entire module
64    pub items: Vec<String>,
65    /// For TypeScript: whether this is a type-only import (import type { ... } from ...)
66    pub is_type_only: bool,
67}
68
69impl Import {
70    /// Create a new import with no specific items (full module import)
71    ///
72    /// # Example
73    ///
74    /// ```
75    /// use spikard_cli::codegen::formatters::Import;
76    /// let import = Import::new("typing");
77    /// assert_eq!(import.module, "typing");
78    /// assert!(import.items.is_empty());
79    /// ```
80    pub fn new(module: impl Into<String>) -> Self {
81        Self {
82            module: module.into(),
83            items: Vec::new(),
84            is_type_only: false,
85        }
86    }
87
88    /// Create an import with specific items
89    ///
90    /// # Example
91    ///
92    /// ```
93    /// use spikard_cli::codegen::formatters::Import;
94    /// let import = Import::with_items("typing", vec!["List", "Dict"]);
95    /// assert_eq!(import.items.len(), 2);
96    /// ```
97    pub fn with_items(module: impl Into<String>, items: Vec<&str>) -> Self {
98        Self {
99            module: module.into(),
100            items: items.iter().map(std::string::ToString::to_string).collect(),
101            is_type_only: false,
102        }
103    }
104
105    /// Mark this import as type-only (TypeScript only)
106    #[must_use]
107    pub const fn with_type_only(mut self, type_only: bool) -> Self {
108        self.is_type_only = type_only;
109        self
110    }
111}
112
113/// Represents a section of generated code to be merged
114#[derive(Debug, Clone)]
115pub enum Section {
116    /// File header (shebang, auto-gen notice, module docstring)
117    Header(String),
118    /// Import statements and require declarations
119    Imports(String),
120    /// Main code body
121    Body(String),
122}
123
124/// Core formatter trait for language-specific code generation output
125///
126/// Implement this trait to support a new target language. Each method should produce
127/// formatted code that adheres to the language's conventions and integrates with its
128/// standard tooling (linters, formatters, type checkers).
129///
130/// # Safety
131///
132/// Implementations must:
133/// - Never panic (return errors via `Result` types where applicable)
134/// - Escape special characters appropriately for the language
135/// - Handle both empty and non-empty inputs gracefully
136/// - Preserve code semantics when reformatting
137pub trait Formatter: Send + Sync {
138    /// Format a file header with metadata about auto-generation
139    ///
140    /// This method should produce language-specific output including:
141    /// - Shebang line (if applicable)
142    /// - Tool directives (ruff: noqa, eslint-disable, etc.)
143    /// - Auto-generation notices
144    /// - Module/file docstrings
145    ///
146    /// The output should NOT include trailing newlines; those are added during merge.
147    fn format_header(&self, metadata: &HeaderMetadata) -> String;
148
149    /// Format import/require/use statements
150    ///
151    /// This method should:
152    /// - Group imports by category (stdlib, third-party, local, type-only)
153    /// - Sort imports alphabetically within each group
154    /// - Handle language-specific syntax (from/import, require, use, etc.)
155    /// - Preserve any special import semantics (type imports in TypeScript, etc.)
156    ///
157    /// Input imports may be in any order; output should be normalized.
158    /// The output should NOT include trailing newlines.
159    fn format_imports(&self, imports: &[Import]) -> String;
160
161    /// Format a docstring with proper escaping and indentation
162    ///
163    /// This method should:
164    /// - Use the language's standard docstring format (triple quotes, `JSDoc`, etc.)
165    /// - Escape any special characters (e.g., triple quotes within the content)
166    /// - Apply proper indentation for nested context
167    /// - Preserve line breaks and formatting in the original content
168    ///
169    /// The output should NOT include trailing newlines.
170    fn format_docstring(&self, content: &str) -> String;
171
172    /// Merge multiple code sections into final output
173    ///
174    /// This method combines header, imports, and body sections with proper spacing:
175    /// - Exactly 2 blank lines between top-level definitions (PEP 8, similar standards)
176    /// - No duplicate headers (if both are present, deduplicate)
177    /// - Ensure trailing newline on final output
178    /// - Handle sections in any order (normalize to header → imports → body)
179    ///
180    /// # Example
181    ///
182    /// The output might look like:
183    /// ```text
184    /// #!/usr/bin/env python3
185    /// # Auto-generated by spikard
186    ///
187    /// from typing import List
188    /// import msgspec
189    ///
190    /// class MyType(msgspec.Struct):
191    ///     fields: List[str]
192    /// ```
193    fn merge_sections(&self, sections: &[Section]) -> String;
194}