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}