Skip to main content

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