Skip to main content

openapi_nexus/codegen/traits/
file_writer.rs

1//! Trait for language-specific file writing operations
2
3use std::collections::HashMap;
4use std::fs;
5
6/// File category for organizing generated files
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub enum FileCategory {
9    /// Represents files without a category. Used internally.
10    /// These files are not written to the output directory.
11    None,
12    /// README or documentation files (e.g., README.md)
13    Readme,
14    /// API client classes
15    Apis,
16    /// Data models and schemas
17    Models,
18    /// Project configuration files (index.ts, package.json, etc.)
19    ProjectFiles,
20    /// Runtime utilities
21    Runtime,
22}
23
24/// Generic file information for writing
25#[derive(Debug, Clone)]
26pub struct FileInfo {
27    pub filename: String,
28    pub content: String,
29    pub category: FileCategory,
30}
31
32impl FileInfo {
33    /// Create a new FileInfo with the specified category
34    pub fn new(filename: String, content: String, category: FileCategory) -> Self {
35        Self {
36            filename,
37            content,
38            category,
39        }
40    }
41
42    /// Create a new FileInfo without a category
43    pub fn none(filename: String, content: String) -> Self {
44        Self::new(filename, content, FileCategory::None)
45    }
46
47    /// Create a new FileInfo for README files
48    pub fn readme(filename: String, content: String) -> Self {
49        Self::new(filename, content, FileCategory::Readme)
50    }
51
52    /// Create a new FileInfo for API files
53    pub fn api(filename: String, content: String) -> Self {
54        Self::new(filename, content, FileCategory::Apis)
55    }
56
57    /// Create a new FileInfo for model files
58    pub fn model(filename: String, content: String) -> Self {
59        Self::new(filename, content, FileCategory::Models)
60    }
61
62    /// Create a new FileInfo for project files
63    pub fn project(filename: String, content: String) -> Self {
64        Self::new(filename, content, FileCategory::ProjectFiles)
65    }
66
67    /// Create a new FileInfo for runtime files
68    pub fn runtime(filename: String, content: String) -> Self {
69        Self::new(filename, content, FileCategory::Runtime)
70    }
71}
72
73/// Trait for language-specific file writing operations
74pub trait FileWriter {
75    /// Optional source directory prefix for languages that use one (e.g. `"src"` for Rust).
76    /// When set, source file categories (`Apis`, `Models`, `Runtime`) are placed
77    /// under `{output_dir}/{source_dir}/` instead of directly under `{output_dir}/`.
78    fn source_dir(&self) -> Option<&str> {
79        None
80    }
81
82    /// Write generated files to the output directory
83    fn write_files(
84        &self,
85        output_dir: &std::path::Path,
86        files: &[FileInfo],
87    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
88        // Group files by category
89        let mut files_by_category: HashMap<FileCategory, Vec<&FileInfo>> = HashMap::new();
90        for file in files {
91            files_by_category
92                .entry(file.category.clone())
93                .or_default()
94                .push(file);
95        }
96
97        let source_root = self
98            .source_dir()
99            .map(|d| output_dir.join(d))
100            .unwrap_or_else(|| output_dir.to_path_buf());
101
102        // Write files for each category
103        for (category, category_files) in files_by_category {
104            let category_dir = match category {
105                FileCategory::None => continue,
106                FileCategory::Readme => output_dir.to_path_buf(),
107                FileCategory::Apis => source_root.join("apis"),
108                FileCategory::Models => source_root.join("models"),
109                FileCategory::ProjectFiles => output_dir.to_path_buf(),
110                FileCategory::Runtime => source_root.join("runtime"),
111            };
112
113            // Create directory if it doesn't exist
114            if !category_dir.exists() {
115                fs::create_dir_all(&category_dir)?;
116            }
117
118            // Write files in this category
119            for file in category_files {
120                let file_path = category_dir.join(&file.filename);
121
122                // Create parent directories if they don't exist (for subdirectories)
123                if let Some(parent) = file_path.parent() {
124                    fs::create_dir_all(parent)?;
125                }
126
127                fs::write(&file_path, &file.content)?;
128            }
129        }
130
131        Ok(())
132    }
133}