openapi_nexus_typescript/templating/
templates.rs

1//! Template name definitions and template emitter
2//! Templates are loaded via minijinja_embed from build.rs
3
4use minijinja::Environment;
5use serde::{Deserialize, Serialize};
6
7use super::environment::create_template_environment;
8use crate::emission::error::EmitError;
9use openapi_nexus_core::traits::FileCategory;
10use openapi_nexus_core::traits::file_writer::FileInfo;
11
12/// Template name enum for type-safe template references
13/// All templates used in the TypeScript generator must be declared here
14/// Organized by FileCategory
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum TemplateName {
17    // FileCategory::Readme
18    /// README documentation template
19    #[serde(rename = "README.md.j2")]
20    Readme,
21
22    // FileCategory::Apis
23    /// Main API class template (generates complete API class files)
24    #[serde(rename = "api/operation.j2")]
25    ApiOperation,
26
27    // FileCategory::Models
28    /// Interface model template
29    #[serde(rename = "model/interface.j2")]
30    ModelInterface,
31    /// Type alias model template
32    #[serde(rename = "model/type_alias.j2")]
33    ModelTypeAlias,
34    /// Enum model template
35    #[serde(rename = "model/enum.j2")]
36    ModelEnum,
37
38    // FileCategory::Runtime
39    /// Runtime utilities template
40    #[serde(rename = "runtime/runtime.j2")]
41    Runtime,
42
43    // FileCategory::ProjectFiles
44    /// Project index file template
45    #[serde(rename = "project/index.j2")]
46    ProjectIndex,
47
48    // FileCategory::None (Snippets/Partials)
49    // These are included by other templates and not rendered directly
50    /// File header template (used across all file types, included by other templates)
51    #[serde(rename = "common/file_header.j2")]
52    CommonFileHeader,
53    /// API method body: Constructor for base API class
54    #[serde(rename = "api/snippets/constructor_base_api.j2")]
55    ApiConstructorBaseApi,
56    /// API method body: GET request handler
57    #[serde(rename = "api/snippets/method_get.j2")]
58    ApiMethodGet,
59    /// API method body: POST/PUT/PATCH request handler
60    #[serde(rename = "api/snippets/method_post_put_patch.j2")]
61    ApiMethodPostPutPatch,
62    /// API method body: DELETE request handler
63    #[serde(rename = "api/snippets/method_delete.j2")]
64    ApiMethodDelete,
65    /// API method body: Convenience wrapper method
66    #[serde(rename = "api/snippets/method_convenience.j2")]
67    ApiMethodConvenience,
68    /// Partial: Build URL path snippet
69    #[serde(rename = "api/snippets/build_url_path.j2")]
70    ApiBuildUrlPath,
71    /// Partial: Build query parameters snippet
72    #[serde(rename = "api/snippets/build_query_params.j2")]
73    ApiBuildQueryParams,
74    /// Partial: Build request headers snippet
75    #[serde(rename = "api/snippets/build_headers.j2")]
76    ApiBuildHeaders,
77    /// Partial: Build request body snippet
78    #[serde(rename = "api/snippets/build_request_body.j2")]
79    ApiBuildRequestBody,
80    /// Partial: Make HTTP request snippet
81    #[serde(rename = "api/snippets/make_request.j2")]
82    ApiMakeRequest,
83    /// Model helper functions template (instanceOf/FromJSON/ToJSON/validation)
84    #[serde(rename = "model/snippets/interface_helpers.j2")]
85    ModelInferenceHelpers,
86}
87
88impl TemplateName {
89    /// Get the file path for this template (used for Minijinja template lookup)
90    pub fn file_path(&self) -> String {
91        serde_plain::to_string(self)
92            .expect("TemplateName should always serialize to a valid string")
93    }
94
95    /// Get the file category for this template
96    pub fn file_category(&self) -> FileCategory {
97        match self {
98            // FileCategory::Readme
99            Self::Readme => FileCategory::Readme,
100
101            // FileCategory::Apis
102            Self::ApiOperation => FileCategory::Apis,
103
104            // FileCategory::Models
105            Self::ModelInterface => FileCategory::Models,
106            Self::ModelTypeAlias => FileCategory::Models,
107            Self::ModelEnum => FileCategory::Models,
108
109            // FileCategory::Runtime
110            Self::Runtime => FileCategory::Runtime,
111
112            // FileCategory::ProjectFiles
113            Self::ProjectIndex => FileCategory::ProjectFiles,
114
115            // FileCategory::None (Snippets/Partials)
116            // These are included by other templates and not rendered directly
117            Self::CommonFileHeader
118            | Self::ApiConstructorBaseApi
119            | Self::ApiMethodGet
120            | Self::ApiMethodPostPutPatch
121            | Self::ApiMethodDelete
122            | Self::ApiMethodConvenience
123            | Self::ApiBuildUrlPath
124            | Self::ApiBuildQueryParams
125            | Self::ApiBuildHeaders
126            | Self::ApiBuildRequestBody
127            | Self::ApiMakeRequest
128            | Self::ModelInferenceHelpers => FileCategory::None,
129        }
130    }
131}
132
133/// Template file path mapping using type-safe enum
134/// Organized by FileCategory for easier tracking
135pub const TEMPLATE_PATHS: &[TemplateName] = &[
136    // FileCategory::Readme
137    TemplateName::Readme,
138    // FileCategory::Apis
139    TemplateName::ApiOperation,
140    // FileCategory::Models
141    TemplateName::ModelEnum,
142    TemplateName::ModelInterface,
143    TemplateName::ModelTypeAlias,
144    // FileCategory::Runtime
145    TemplateName::Runtime,
146    // FileCategory::ProjectFiles
147    TemplateName::ProjectIndex,
148    // FileCategory::None (Snippets/Partials)
149    TemplateName::ApiBuildHeaders,
150    TemplateName::ApiBuildQueryParams,
151    TemplateName::ApiBuildRequestBody,
152    TemplateName::ApiBuildUrlPath,
153    TemplateName::ApiConstructorBaseApi,
154    TemplateName::ApiMakeRequest,
155    TemplateName::ApiMethodConvenience,
156    TemplateName::ApiMethodDelete,
157    TemplateName::ApiMethodGet,
158    TemplateName::ApiMethodPostPutPatch,
159    TemplateName::CommonFileHeader,
160    TemplateName::ModelInferenceHelpers,
161];
162
163/// Template-based TypeScript code emitter and template handler
164/// Templates are loaded via minijinja_embed from build.rs
165#[derive(Debug, Clone)]
166pub struct Templates {
167    env: Environment<'static>,
168}
169
170impl Default for Templates {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176impl Templates {
177    /// Create a new template handler with initialized templates
178    /// Each instance has its own Environment (not shared)
179    /// Templates are loaded via minijinja_embed from build.rs
180    pub fn new() -> Self {
181        let env = create_template_environment();
182        Self { env }
183    }
184
185    pub fn render_template(
186        &self,
187        template_name: TemplateName,
188        output_filename: &str,
189        context: minijinja::Value,
190    ) -> Result<FileInfo, EmitError> {
191        let template_path = template_name.file_path();
192        let template = self.env.get_template(&template_path).map_err(|e| {
193            let err = EmitError::TemplateError {
194                message: format!("Failed to get {} template: {}", template_path, e),
195            };
196            tracing::error!("{}", err);
197            err
198        })?;
199        let content = template.render(context).map_err(|e| {
200            let err = EmitError::TemplateError {
201                message: format!("Failed to render {} template: {}", template_path, e),
202            };
203            tracing::error!("{}", err);
204            err
205        })?;
206
207        Ok(FileInfo::new(
208            output_filename.to_string(),
209            content,
210            template_name.file_category(),
211        ))
212    }
213
214    /// Render a template and return the content as a string
215    pub fn render_template_string(
216        &self,
217        template_name: TemplateName,
218        context: minijinja::Value,
219    ) -> Result<String, EmitError> {
220        let template_path = template_name.file_path();
221        let template = self.env.get_template(&template_path).map_err(|e| {
222            let err = EmitError::TemplateError {
223                message: format!("Failed to get {} template: {}", template_path, e),
224            };
225            tracing::error!("{}", err);
226            err
227        })?;
228        template.render(context).map_err(|e| {
229            let err = EmitError::TemplateError {
230                message: format!("Failed to render {} template: {}", template_path, e),
231            };
232            tracing::error!("{}", err);
233            err
234        })
235    }
236}