Skip to main content

unistructgen_codegen/
lib.rs

1mod builder;
2mod json_schema;
3
4pub use builder::RustRendererBuilder;
5pub use json_schema::{JsonSchemaRenderer, JsonSchemaError};
6
7use std::fmt::Write;
8use thiserror::Error;
9use unistructgen_core::{CodeGenerator, GeneratorMetadata, IREnum, IRField, IRModule, IRStruct, IRType, IRTypeRef};
10
11/// Errors that can occur during code generation
12///
13/// Each error variant provides detailed context about what went wrong
14/// during the code generation process.
15#[derive(Error, Debug)]
16pub enum CodegenError {
17    /// Failed to render a specific part of the code
18    ///
19    /// This typically indicates a bug in the generator logic.
20    #[error("Rendering error for {component} in {context}: {message}")]
21    RenderError {
22        /// The component that failed to render (e.g., "struct", "field", "enum")
23        component: String,
24        /// Context where the error occurred (e.g., "User", "Address")
25        context: String,
26        /// Description of what went wrong
27        message: String,
28    },
29
30    /// String formatting error
31    ///
32    /// This occurs when writing to the output buffer fails.
33    #[error("Format error while rendering {context}: {source}")]
34    FormatError {
35        /// Context where formatting failed
36        context: String,
37        /// The underlying fmt::Error
38        #[source]
39        source: std::fmt::Error,
40    },
41
42    /// Module validation failed
43    ///
44    /// The IR module contains invalid data that cannot be generated.
45    #[error("Validation error: {reason}")]
46    ValidationError {
47        /// Reason why validation failed
48        reason: String,
49        /// Optional suggestion for fixing the issue
50        suggestion: Option<String>,
51    },
52
53    /// Empty or invalid identifier name
54    #[error("Invalid identifier '{name}' in {context}: {reason}")]
55    InvalidIdentifier {
56        /// The invalid identifier name
57        name: String,
58        /// Context where the identifier is used (e.g., "struct name", "field name")
59        context: String,
60        /// Reason why it's invalid
61        reason: String,
62    },
63
64    /// Unsupported type encountered
65    #[error("Unsupported type '{type_name}' in {context}: {reason}")]
66    UnsupportedType {
67        /// The type that is not supported
68        type_name: String,
69        /// Context where the type was encountered
70        context: String,
71        /// Why it's not supported
72        reason: String,
73        /// Optional alternative or workaround
74        alternative: Option<String>,
75    },
76
77    /// Maximum recursion depth exceeded
78    ///
79    /// Prevents stack overflow from deeply nested types.
80    #[error("Maximum recursion depth of {max_depth} exceeded while rendering {context}")]
81    MaxDepthExceeded {
82        /// Context where max depth was reached
83        context: String,
84        /// The maximum allowed depth
85        max_depth: usize,
86    },
87}
88
89impl CodegenError {
90    /// Create a render error
91    #[allow(dead_code)]
92    pub(crate) fn render_error(
93        component: impl Into<String>,
94        context: impl Into<String>,
95        message: impl Into<String>,
96    ) -> Self {
97        Self::RenderError {
98            component: component.into(),
99            context: context.into(),
100            message: message.into(),
101        }
102    }
103
104    /// Create a validation error
105    pub(crate) fn validation_error(reason: impl Into<String>) -> Self {
106        Self::ValidationError {
107            reason: reason.into(),
108            suggestion: None,
109        }
110    }
111
112    /// Add a suggestion to an error
113    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
114        let suggestion = suggestion.into();
115        match &mut self {
116            Self::ValidationError { suggestion: s, .. } => {
117                *s = Some(suggestion);
118            }
119            Self::UnsupportedType { alternative, .. } => {
120                *alternative = Some(suggestion);
121            }
122            _ => {}
123        }
124        self
125    }
126
127    /// Create an invalid identifier error
128    pub(crate) fn invalid_identifier(
129        name: impl Into<String>,
130        context: impl Into<String>,
131        reason: impl Into<String>,
132    ) -> Self {
133        Self::InvalidIdentifier {
134            name: name.into(),
135            context: context.into(),
136            reason: reason.into(),
137        }
138    }
139}
140
141impl From<std::fmt::Error> for CodegenError {
142    fn from(err: std::fmt::Error) -> Self {
143        Self::FormatError {
144            context: "unknown".to_string(),
145            source: err,
146        }
147    }
148}
149
150pub type Result<T> = std::result::Result<T, CodegenError>;
151
152#[derive(Debug, Clone)]
153pub struct RenderOptions {
154    pub add_header: bool,
155    pub add_clippy_allows: bool,
156}
157
158impl Default for RenderOptions {
159    fn default() -> Self {
160        Self {
161            add_header: true,
162            add_clippy_allows: true,
163        }
164    }
165}
166
167/// Rust code generator that converts IR to idiomatic Rust code
168///
169/// This generator implements the [`CodeGenerator`] trait and produces
170/// clean, well-formatted Rust code with proper derives and attributes.
171///
172/// # Features
173///
174/// - **Derive Macros**: Automatically adds Debug, Clone, PartialEq, and optional serde derives
175/// - **Nested Types**: Generates all nested struct definitions
176/// - **Documentation**: Preserves doc comments from IR
177/// - **Formatting**: Produces properly indented, formatted code
178/// - **Type Safety**: Maps IR types to appropriate Rust types
179///
180/// # Examples
181///
182/// ```
183/// use unistructgen_codegen::{RustRenderer, RenderOptions};
184/// use unistructgen_core::{CodeGenerator, IRModule, IRStruct, IRType};
185///
186/// let mut module = IRModule::new("User".to_string());
187/// let user_struct = IRStruct::new("User".to_string());
188/// module.add_type(IRType::Struct(user_struct));
189///
190/// let renderer = RustRenderer::new(RenderOptions::default());
191/// let code = renderer.generate(&module).expect("Failed to generate");
192/// assert!(code.contains("pub struct User"));
193/// ```
194pub struct RustRenderer {
195    options: RenderOptions,
196}
197
198impl RustRenderer {
199    pub fn new(options: RenderOptions) -> Self {
200        Self { options }
201    }
202
203    pub fn render(&self, module: &IRModule) -> Result<String> {
204        let mut output = String::new();
205
206        if self.options.add_header {
207            writeln!(output, "// Generated by unistructgen v{}", env!("CARGO_PKG_VERSION"))?;
208            writeln!(output, "// Do not edit this file manually")?;
209            writeln!(output)?;
210        }
211
212        if self.options.add_clippy_allows {
213            writeln!(output, "#![allow(dead_code)]")?;
214            writeln!(output, "#![allow(unused_imports)]")?;
215            writeln!(output)?;
216        }
217
218        for ty in &module.types {
219            match ty {
220                IRType::Struct(s) => {
221                    self.render_struct(&mut output, s)?;
222                    writeln!(output)?;
223                }
224                IRType::Enum(e) => {
225                    self.render_enum(&mut output, e)?;
226                    writeln!(output)?;
227                }
228            }
229        }
230
231        Ok(output)
232    }
233
234    fn render_struct(&self, output: &mut String, ir_struct: &IRStruct) -> Result<()> {
235        // Doc comment
236        if let Some(doc) = &ir_struct.doc {
237            writeln!(output, "/// {}", doc)?;
238        }
239
240        // Derives
241        if !ir_struct.derives.is_empty() {
242            write!(output, "#[derive(")?;
243            for (i, derive) in ir_struct.derives.iter().enumerate() {
244                if i > 0 {
245                    write!(output, ", ")?;
246                }
247                write!(output, "{}", derive)?;
248            }
249            writeln!(output, ")]")?;
250        }
251
252        // Additional attributes
253        for attr in &ir_struct.attributes {
254            writeln!(output, "#[{}]", attr)?;
255        }
256
257        writeln!(output, "pub struct {} {{", ir_struct.name)?;
258
259        for field in &ir_struct.fields {
260            self.render_field(output, field)?;
261        }
262
263        writeln!(output, "}}")?;
264
265        Ok(())
266    }
267
268    fn render_field(&self, output: &mut String, field: &IRField) -> Result<()> {
269        // Field doc
270        if let Some(doc) = &field.doc {
271            writeln!(output, "    /// {}", doc)?;
272        }
273
274        // Field attributes
275        for attr in &field.attributes {
276            writeln!(output, "    #[{}]", attr)?;
277        }
278
279        // Generate validation attributes from constraints
280        let validation_attrs = self.generate_validation_attrs(&field.constraints);
281        if !validation_attrs.is_empty() {
282            writeln!(output, "    #[validate({})]", validation_attrs.join(", "))?;
283        }
284
285        // Field definition
286        writeln!(
287            output,
288            "    pub {}: {},",
289            field.name,
290            self.render_type(&field.ty)?
291        )?;
292
293        Ok(())
294    }
295
296    /// Generate validation attributes from field constraints
297    fn generate_validation_attrs(&self, constraints: &unistructgen_core::FieldConstraints) -> Vec<String> {
298        let mut attrs = Vec::new();
299
300        // Length constraints (for strings and arrays)
301        if constraints.min_length.is_some() || constraints.max_length.is_some() {
302            let mut length_parts = Vec::new();
303            if let Some(min) = constraints.min_length {
304                length_parts.push(format!("min = {}", min));
305            }
306            if let Some(max) = constraints.max_length {
307                length_parts.push(format!("max = {}", max));
308            }
309            attrs.push(format!("length({})", length_parts.join(", ")));
310        }
311
312        // Numeric range constraints
313        if constraints.min_value.is_some() || constraints.max_value.is_some() {
314            let mut range_parts = Vec::new();
315            if let Some(min) = constraints.min_value {
316                // Handle both integer and float values
317                if min.fract() == 0.0 {
318                    range_parts.push(format!("min = {}", min as i64));
319                } else {
320                    range_parts.push(format!("min = {}", min));
321                }
322            }
323            if let Some(max) = constraints.max_value {
324                if max.fract() == 0.0 {
325                    range_parts.push(format!("max = {}", max as i64));
326                } else {
327                    range_parts.push(format!("max = {}", max));
328                }
329            }
330            attrs.push(format!("range({})", range_parts.join(", ")));
331        }
332
333        // Pattern/regex constraints
334        if let Some(pattern) = &constraints.pattern {
335            attrs.push(format!("regex = \"{}\"", pattern.replace('\"', "\\\"")));
336        }
337
338        // Email format
339        if let Some(format) = &constraints.format {
340            match format.as_str() {
341                "email" => attrs.push("email".to_string()),
342                "url" => attrs.push("url".to_string()),
343                _ => {} // Other formats not directly supported by validator crate
344            }
345        }
346
347        attrs
348    }
349
350    fn render_enum(&self, output: &mut String, ir_enum: &IREnum) -> Result<()> {
351        // Doc comment
352        if let Some(doc) = &ir_enum.doc {
353            writeln!(output, "/// {}", doc)?;
354        }
355
356        // Derives
357        if !ir_enum.derives.is_empty() {
358            write!(output, "#[derive(")?;
359            for (i, derive) in ir_enum.derives.iter().enumerate() {
360                if i > 0 {
361                    write!(output, ", ")?;
362                }
363                write!(output, "{}", derive)?;
364            }
365            writeln!(output, ")]")?;
366        }
367
368        writeln!(output, "pub enum {} {{", ir_enum.name)?;
369
370        for variant in &ir_enum.variants {
371            if let Some(doc) = &variant.doc {
372                writeln!(output, "    /// {}", doc)?;
373            }
374            // Add serde(rename) if the variant name differs from source value
375            if let Some(source_value) = &variant.source_value {
376                writeln!(output, "    #[serde(rename = \"{}\")]", source_value)?;
377            }
378            writeln!(output, "    {},", variant.name)?;
379        }
380
381        writeln!(output, "}}")?;
382
383        Ok(())
384    }
385
386    fn render_type(&self, ty: &IRTypeRef) -> Result<String> {
387        match ty {
388            IRTypeRef::Primitive(p) => Ok(p.rust_type_name().to_string()),
389            IRTypeRef::Option(inner) => {
390                Ok(format!("Option<{}>", self.render_type(inner)?))
391            }
392            IRTypeRef::Vec(inner) => {
393                Ok(format!("Vec<{}>", self.render_type(inner)?))
394            }
395            IRTypeRef::Named(name) => Ok(name.clone()),
396            IRTypeRef::Map(key, value) => {
397                Ok(format!(
398                    "std::collections::HashMap<{}, {}>",
399                    self.render_type(key)?,
400                    self.render_type(value)?
401                ))
402            }
403        }
404    }
405}
406
407// Implementation of CodeGenerator trait for RustRenderer
408impl CodeGenerator for RustRenderer {
409    type Error = CodegenError;
410
411    fn generate(&self, module: &IRModule) -> std::result::Result<String, Self::Error> {
412        self.render(module)
413    }
414
415    fn language(&self) -> &'static str {
416        "Rust"
417    }
418
419    fn file_extension(&self) -> &str {
420        "rs"
421    }
422
423    fn validate(&self, module: &IRModule) -> std::result::Result<(), Self::Error> {
424        // Basic validation: ensure module has at least one type
425        if module.types.is_empty() {
426            return Err(CodegenError::validation_error(
427                "Module must contain at least one type"
428            ).with_suggestion(
429                "Ensure the parser generates at least one struct or enum"
430            ));
431        }
432
433        // Validate that all types have valid names
434        for ty in &module.types {
435            match ty {
436                IRType::Struct(s) => {
437                    if s.name.is_empty() {
438                        return Err(CodegenError::invalid_identifier(
439                            "",
440                            "struct name",
441                            "name cannot be empty",
442                        ));
443                    }
444                }
445                IRType::Enum(e) => {
446                    if e.name.is_empty() {
447                        return Err(CodegenError::invalid_identifier(
448                            "",
449                            "enum name",
450                            "name cannot be empty",
451                        ));
452                    }
453                }
454            }
455        }
456
457        Ok(())
458    }
459
460    fn format(&self, code: String) -> std::result::Result<String, Self::Error> {
461        // For now, just return the code as-is
462        // In the future, could integrate with rustfmt
463        Ok(code)
464    }
465
466    fn metadata(&self) -> GeneratorMetadata {
467        GeneratorMetadata::new()
468            .with_version(env!("CARGO_PKG_VERSION"))
469            .with_description("Generates idiomatic Rust code with derives and documentation")
470            .with_min_language_version("1.70")
471            .with_feature("derive-macros")
472            .with_feature("nested-types")
473            .with_feature("serde-support")
474            .with_feature("doc-comments")
475            .with_feature("type-safety")
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use unistructgen_core::{IRField, PrimitiveKind};
483
484    #[test]
485    fn test_render_simple_struct() {
486        let mut ir_struct = IRStruct::new("User".to_string());
487        ir_struct.add_field(IRField::new(
488            "id".to_string(),
489            IRTypeRef::Primitive(PrimitiveKind::I64),
490        ));
491        ir_struct.add_field(IRField::new(
492            "name".to_string(),
493            IRTypeRef::Primitive(PrimitiveKind::String),
494        ));
495
496        let mut module = IRModule::new("test".to_string());
497        module.add_type(IRType::Struct(ir_struct));
498
499        let renderer = RustRenderer::new(RenderOptions::default());
500        let output = renderer.render(&module).unwrap();
501
502        assert!(output.contains("pub struct User"));
503        assert!(output.contains("pub id: i64"));
504        assert!(output.contains("pub name: String"));
505    }
506
507    #[test]
508    fn test_render_optional_field() {
509        let mut ir_struct = IRStruct::new("User".to_string());
510        ir_struct.add_field(IRField::new(
511            "email".to_string(),
512            IRTypeRef::Option(Box::new(IRTypeRef::Primitive(PrimitiveKind::String))),
513        ));
514
515        let mut module = IRModule::new("test".to_string());
516        module.add_type(IRType::Struct(ir_struct));
517
518        let renderer = RustRenderer::new(RenderOptions::default());
519        let output = renderer.render(&module).unwrap();
520
521        assert!(output.contains("pub email: Option<String>"));
522    }
523
524    #[test]
525    fn test_render_vec_field() {
526        let mut ir_struct = IRStruct::new("User".to_string());
527        ir_struct.add_field(IRField::new(
528            "tags".to_string(),
529            IRTypeRef::Vec(Box::new(IRTypeRef::Primitive(PrimitiveKind::String))),
530        ));
531
532        let mut module = IRModule::new("test".to_string());
533        module.add_type(IRType::Struct(ir_struct));
534
535        let renderer = RustRenderer::new(RenderOptions::default());
536        let output = renderer.render(&module).unwrap();
537
538        assert!(output.contains("pub tags: Vec<String>"));
539    }
540}