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#[derive(Error, Debug)]
16pub enum CodegenError {
17 #[error("Rendering error for {component} in {context}: {message}")]
21 RenderError {
22 component: String,
24 context: String,
26 message: String,
28 },
29
30 #[error("Format error while rendering {context}: {source}")]
34 FormatError {
35 context: String,
37 #[source]
39 source: std::fmt::Error,
40 },
41
42 #[error("Validation error: {reason}")]
46 ValidationError {
47 reason: String,
49 suggestion: Option<String>,
51 },
52
53 #[error("Invalid identifier '{name}' in {context}: {reason}")]
55 InvalidIdentifier {
56 name: String,
58 context: String,
60 reason: String,
62 },
63
64 #[error("Unsupported type '{type_name}' in {context}: {reason}")]
66 UnsupportedType {
67 type_name: String,
69 context: String,
71 reason: String,
73 alternative: Option<String>,
75 },
76
77 #[error("Maximum recursion depth of {max_depth} exceeded while rendering {context}")]
81 MaxDepthExceeded {
82 context: String,
84 max_depth: usize,
86 },
87}
88
89impl CodegenError {
90 #[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 pub(crate) fn validation_error(reason: impl Into<String>) -> Self {
106 Self::ValidationError {
107 reason: reason.into(),
108 suggestion: None,
109 }
110 }
111
112 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 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
167pub 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 if let Some(doc) = &ir_struct.doc {
237 writeln!(output, "/// {}", doc)?;
238 }
239
240 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 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 if let Some(doc) = &field.doc {
271 writeln!(output, " /// {}", doc)?;
272 }
273
274 for attr in &field.attributes {
276 writeln!(output, " #[{}]", attr)?;
277 }
278
279 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 writeln!(
287 output,
288 " pub {}: {},",
289 field.name,
290 self.render_type(&field.ty)?
291 )?;
292
293 Ok(())
294 }
295
296 fn generate_validation_attrs(&self, constraints: &unistructgen_core::FieldConstraints) -> Vec<String> {
298 let mut attrs = Vec::new();
299
300 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 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 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 if let Some(pattern) = &constraints.pattern {
335 attrs.push(format!("regex = \"{}\"", pattern.replace('\"', "\\\"")));
336 }
337
338 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 _ => {} }
345 }
346
347 attrs
348 }
349
350 fn render_enum(&self, output: &mut String, ir_enum: &IREnum) -> Result<()> {
351 if let Some(doc) = &ir_enum.doc {
353 writeln!(output, "/// {}", doc)?;
354 }
355
356 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 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
407impl 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 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 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 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}