vika_cli/generator/
query_params.rs

1use crate::error::Result;
2use crate::generator::api_client::{extract_query_parameters, ParameterType};
3use crate::generator::swagger_parser::OperationInfo;
4use crate::generator::ts_typings::TypeScriptType;
5use crate::generator::utils::to_pascal_case;
6use crate::generator::zod_schema::ZodSchema;
7use crate::templates::context::{Field, TypeContext, ZodContext};
8use crate::templates::engine::TemplateEngine;
9use crate::templates::registry::TemplateId;
10use openapiv3::OpenAPI;
11use std::collections::HashMap;
12
13pub struct QueryParamsGenerationResult {
14    pub types: Vec<TypeScriptType>,
15    pub zod_schemas: Vec<ZodSchema>,
16}
17
18/// Context for generating query params
19pub struct QueryParamsContext<'a> {
20    pub openapi: &'a OpenAPI,
21    pub operations: &'a [OperationInfo],
22    pub enum_registry: &'a mut HashMap<String, String>,
23    pub template_engine: Option<&'a TemplateEngine>,
24    pub spec_name: Option<&'a str>,
25    pub existing_types: &'a [TypeScriptType],
26    pub existing_zod_schemas: &'a [ZodSchema],
27}
28
29/// Generate query params types and Zod schemas for all operations in a module
30pub fn generate_query_params_for_module(
31    ctx: QueryParamsContext,
32) -> Result<QueryParamsGenerationResult> {
33    let QueryParamsContext {
34        openapi,
35        operations,
36        enum_registry,
37        template_engine,
38        spec_name,
39        existing_types,
40        existing_zod_schemas,
41    } = ctx;
42    let mut types = Vec::new();
43    let mut zod_schemas = Vec::new();
44
45    // Build a set of existing type names to avoid duplicates
46    let mut existing_type_names = std::collections::HashSet::new();
47    for t in existing_types {
48        // Extract type name from content: "export type XEnum = ..." or "export interface X { ... }"
49        if let Some(name) = extract_type_name(&t.content) {
50            existing_type_names.insert(name);
51        }
52    }
53
54    // Build a set of existing schema names to avoid duplicates
55    let mut existing_schema_names = std::collections::HashSet::new();
56    for z in existing_zod_schemas {
57        // Extract schema name from content: "export const XSchema = ..."
58        if let Some(name) = extract_schema_name(&z.content) {
59            existing_schema_names.insert(name);
60        }
61    }
62
63    // Helper function to extract type name from content
64    fn extract_type_name(content: &str) -> Option<String> {
65        content.find("export type ").and_then(|start| {
66            let after_export = &content[start + 12..];
67            after_export
68                .find([' ', '=', '{'])
69                .map(|end| after_export[..end].trim().to_string())
70        })
71    }
72
73    // Helper function to extract schema name from content
74    fn extract_schema_name(content: &str) -> Option<String> {
75        content.find("export const ").and_then(|start| {
76            let after_export = &content[start + 13..];
77            after_export
78                .find([' ', '=', ':'])
79                .map(|end| after_export[..end].trim().to_string())
80        })
81    }
82
83    for op_info in operations {
84        let operation = &op_info.operation;
85        let query_params = extract_query_parameters(openapi, operation, enum_registry)?;
86
87        if query_params.is_empty() {
88            continue;
89        }
90
91        // Extract operation_id from operation
92        let func_name = operation.operation_id.clone().unwrap_or_else(|| {
93            // Fallback: generate from method and path
94            format!(
95                "{}{}",
96                to_pascal_case(&op_info.method),
97                to_pascal_case(
98                    &op_info
99                        .path
100                        .replace("/", "")
101                        .replace("{", "")
102                        .replace("}", "")
103                )
104            )
105        });
106        let type_name_base = to_pascal_case(&func_name);
107        let query_type_name = format!("{}QueryParams", type_name_base);
108        // Schema name without "Schema" suffix - template will add it
109        let schema_name = query_type_name.clone();
110
111        // Collect enum types from query params and generate them
112        // We need to track which enums we've already generated in this module to avoid duplicates
113        // Skip enums that already exist from schema definitions
114        let mut enum_types_to_generate = Vec::new();
115        let mut generated_enum_names = std::collections::HashSet::new();
116
117        for param in &query_params {
118            if let ParameterType::Enum(enum_name) = &param.param_type {
119                if let Some(enum_values) = &param.enum_values {
120                    // Skip if enum type already exists from schema definitions
121                    let enum_schema_name = format!("{}Schema", enum_name);
122                    if !generated_enum_names.contains(enum_name)
123                        && !existing_type_names.contains(enum_name)
124                        && !existing_schema_names.contains(&enum_schema_name)
125                    {
126                        enum_types_to_generate.push((enum_name.clone(), enum_values.clone()));
127                        generated_enum_names.insert(enum_name.clone());
128                        // Register the enum to prevent duplicate generation within this module
129                        let enum_key = enum_values.join(",");
130                        enum_registry.insert(enum_key, enum_name.clone());
131                    }
132                }
133            }
134        }
135
136        // Generate enum types first (before query params interface)
137        for (enum_name, enum_values) in &enum_types_to_generate {
138            // Generate TypeScript enum type
139            let enum_type_content = format!(
140                "export type {} =\n{}\n;",
141                enum_name,
142                enum_values
143                    .iter()
144                    .map(|v| format!("\"{}\"", v))
145                    .collect::<Vec<_>>()
146                    .join(" |\n")
147            );
148            types.push(TypeScriptType {
149                content: enum_type_content,
150            });
151
152            // Generate Zod enum schema
153            // Template adds "Schema" suffix, so we pass just the enum name
154            // The final schema name will be "{enum_name}Schema"
155            if let Some(engine) = template_engine {
156                let zod_context = ZodContext {
157                    schema_name: enum_name.clone(),
158                    zod_expr: format!(
159                        "z.enum([{}])",
160                        enum_values
161                            .iter()
162                            .map(|v| format!("\"{}\"", v))
163                            .collect::<Vec<_>>()
164                            .join(", ")
165                    ),
166                    is_enum: true,
167                    enum_values: Some(enum_values.clone()),
168                    description: None,
169                    needs_type_annotation: false,
170                    spec_name: spec_name.map(|s| s.to_string()),
171                };
172                let zod_content = engine.render(TemplateId::ZodEnum, &zod_context)?;
173                zod_schemas.push(ZodSchema {
174                    content: zod_content,
175                });
176            } else {
177                // Fallback without template
178                let enum_values_str = enum_values
179                    .iter()
180                    .map(|v| format!("\"{}\"", v))
181                    .collect::<Vec<_>>()
182                    .join(", ");
183                zod_schemas.push(ZodSchema {
184                    content: format!(
185                        "export const {}Schema = z.enum([{}]);",
186                        enum_name, enum_values_str
187                    ),
188                });
189            }
190        }
191
192        // Generate TypeScript interface fields
193        let mut fields = Vec::new();
194        for param in &query_params {
195            let param_type = match &param.param_type {
196                ParameterType::Enum(enum_name) => enum_name.clone(),
197                ParameterType::Array(item_type) => format!("{}[]", item_type),
198                ParameterType::String => "string".to_string(),
199                ParameterType::Number => "number".to_string(),
200                ParameterType::Integer => "number".to_string(),
201                ParameterType::Boolean => "boolean".to_string(),
202            };
203
204            fields.push(Field {
205                name: param.name.clone(),
206                type_name: param_type,
207                optional: true,
208                description: param.description.clone(),
209            });
210        }
211
212        // Generate TypeScript type using template
213        if let Some(engine) = template_engine {
214            let context = TypeContext::interface(
215                query_type_name.clone(),
216                fields,
217                None,
218                spec_name.map(|s| s.to_string()),
219            );
220            let content = engine.render(TemplateId::TypeInterface, &context)?;
221            types.push(TypeScriptType { content });
222        } else {
223            // Fallback without template
224            let mut field_strings = Vec::new();
225            for field in &fields {
226                let desc = field
227                    .description
228                    .as_ref()
229                    .map(|d| format!("  /**\n   * {}\n   */\n  ", d))
230                    .unwrap_or_default();
231                field_strings.push(format!("{}{}?: {};", desc, field.name, field.type_name));
232            }
233            types.push(TypeScriptType {
234                content: format!(
235                    "export interface {} {{\n{}\n}}",
236                    query_type_name,
237                    field_strings.join("\n")
238                ),
239            });
240        }
241
242        // Generate Zod schema expression
243        let mut zod_field_strings = Vec::new();
244        for param in &query_params {
245            let zod_type = match &param.param_type {
246                ParameterType::Enum(enum_name) => {
247                    // For enums, use the enum schema that was generated above
248                    // Enum schemas are in the same file (schemas.ts), so reference directly
249                    // Template adds "Schema" suffix, so enum schema name is "{enum_name}Schema"
250                    format!("{}Schema", enum_name)
251                }
252                ParameterType::Array(item_type) => {
253                    match item_type.as_str() {
254                        "string" => "z.array(z.string())".to_string(),
255                        "number" => "z.array(z.number())".to_string(),
256                        "boolean" => "z.array(z.boolean())".to_string(),
257                        _ => "z.array(z.any())".to_string(), // For custom types
258                    }
259                }
260                ParameterType::String => "z.string()".to_string(),
261                ParameterType::Number => "z.number()".to_string(),
262                ParameterType::Integer => "z.number()".to_string(),
263                ParameterType::Boolean => "z.boolean()".to_string(),
264            };
265
266            let optional_zod = format!("{}.optional()", zod_type);
267            zod_field_strings.push(format!("  {}: {},", param.name, optional_zod));
268        }
269
270        let zod_expr = format!("z.object({{\n{}\n}})", zod_field_strings.join("\n"));
271
272        // Generate Zod schema using template
273        if let Some(engine) = template_engine {
274            // Template adds "Schema" suffix, so we pass the base name without it
275            let zod_context = ZodContext {
276                schema_name: schema_name.clone(),
277                zod_expr,
278                is_enum: false,
279                enum_values: None,
280                description: None,
281                needs_type_annotation: false,
282                spec_name: spec_name.map(|s| s.to_string()),
283            };
284            let zod_content = engine.render(TemplateId::ZodSchema, &zod_context)?;
285            zod_schemas.push(ZodSchema {
286                content: zod_content,
287            });
288        } else {
289            // Fallback without template
290            zod_schemas.push(ZodSchema {
291                content: format!("export const {} = {};", schema_name, zod_expr),
292            });
293        }
294    }
295
296    Ok(QueryParamsGenerationResult { types, zod_schemas })
297}