vika_cli/generator/hooks/
react_query.rs1use crate::error::Result;
2use crate::generator::api_client::{
3 extract_all_responses, extract_path_parameters, extract_query_parameters, extract_request_body,
4 ResponseInfo,
5};
6use crate::generator::hooks::context::HookContext;
7use crate::generator::hooks::HookFile;
8use crate::generator::swagger_parser::OperationInfo;
9use crate::generator::utils::{to_camel_case, to_pascal_case};
10use crate::templates::context::Parameter as ApiParameter;
11use crate::templates::engine::TemplateEngine;
12use crate::templates::registry::TemplateId;
13use openapiv3::OpenAPI;
14
15pub fn generate_react_query_hooks(
17 openapi: &OpenAPI,
18 operations: &[OperationInfo],
19 module_name: &str,
20 spec_name: Option<&str>,
21 common_schemas: &[String],
22 enum_registry: &mut std::collections::HashMap<String, String>,
23 template_engine: &TemplateEngine,
24) -> Result<Vec<HookFile>> {
25 let mut hooks = Vec::new();
26
27 for op_info in operations {
28 let operation = &op_info.operation;
29 let method = op_info.method.to_uppercase();
30
31 let is_query = matches!(method.as_str(), "GET" | "HEAD");
33 let is_mutation = matches!(method.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
34
35 if !is_query && !is_mutation {
36 continue; }
38
39 let operation_id = if let Some(op_id) = &operation.operation_id {
41 to_camel_case(op_id)
42 } else {
43 generate_hook_name_from_path(&op_info.path, &op_info.method)
44 };
45
46 let hook_name = format!("use{}", to_pascal_case(&operation_id));
48
49 let key_name = operation_id.clone();
51
52 let path_params_info = extract_path_parameters(openapi, operation, enum_registry)?;
54 let query_params_info = extract_query_parameters(openapi, operation, enum_registry)?;
55 let request_body_info = extract_request_body(openapi, operation)?;
56 let all_responses = extract_all_responses(openapi, operation)?;
57
58 let success_responses: Vec<ResponseInfo> = all_responses
60 .iter()
61 .filter(|r| r.status_code >= 200 && r.status_code < 300)
62 .cloned()
63 .collect();
64 let response_type = success_responses
65 .iter()
66 .find(|r| r.status_code == 200)
67 .map(|r| r.body_type.clone())
68 .unwrap_or_else(|| "any".to_string());
69
70 let mut param_list_parts = Vec::new();
72 let mut param_names_parts = Vec::new();
73
74 for param in &path_params_info {
76 let param_type = match ¶m.param_type {
77 crate::generator::api_client::ParameterType::Enum(enum_name) => enum_name.clone(),
78 crate::generator::api_client::ParameterType::String => "string".to_string(),
79 crate::generator::api_client::ParameterType::Number => "number".to_string(),
80 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
81 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
82 crate::generator::api_client::ParameterType::Array(_) => "string".to_string(),
83 };
84 param_list_parts.push(format!("{}: {}", param.name, param_type));
85 param_names_parts.push(param.name.clone());
86 }
87
88 let mut enum_types = Vec::new();
90
91 if is_query && !query_params_info.is_empty() {
93 let mut query_fields = Vec::new();
94 for param in &query_params_info {
95 let param_type = match ¶m.param_type {
96 crate::generator::api_client::ParameterType::Enum(enum_name) => {
97 enum_types.push(enum_name.clone());
98 enum_name.clone()
99 }
100 crate::generator::api_client::ParameterType::Array(item_type) => {
101 format!("{}[]", item_type)
102 }
103 crate::generator::api_client::ParameterType::String => "string".to_string(),
104 crate::generator::api_client::ParameterType::Number => "number".to_string(),
105 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
106 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
107 };
108 query_fields.push(format!("{}?: {}", param.name, param_type));
109 }
110 let query_type = format!("{{ {} }}", query_fields.join(", "));
111 param_list_parts.push(format!("query?: {}", query_type));
112 param_names_parts.push("query".to_string());
113 }
114
115 let param_list = param_list_parts.join(", ");
120 let param_names = param_names_parts.join(", ");
121
122 let path_params: Vec<ApiParameter> = path_params_info
124 .iter()
125 .map(|p| {
126 let param_type = match &p.param_type {
127 crate::generator::api_client::ParameterType::Enum(enum_name) => {
128 enum_name.clone()
129 }
130 crate::generator::api_client::ParameterType::Array(item_type) => {
131 format!("{}[]", item_type)
132 }
133 crate::generator::api_client::ParameterType::String => "string".to_string(),
134 crate::generator::api_client::ParameterType::Number => "number".to_string(),
135 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
136 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
137 };
138 ApiParameter::new(p.name.clone(), param_type, false, p.description.clone())
139 })
140 .collect();
141
142 let query_params: Vec<ApiParameter> = query_params_info
143 .iter()
144 .map(|p| {
145 let param_type = match &p.param_type {
146 crate::generator::api_client::ParameterType::Enum(enum_name) => {
147 enum_name.clone()
148 }
149 crate::generator::api_client::ParameterType::Array(item_type) => {
150 format!("{}[]", item_type)
151 }
152 crate::generator::api_client::ParameterType::String => "string".to_string(),
153 crate::generator::api_client::ParameterType::Number => "number".to_string(),
154 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
155 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
156 };
157 ApiParameter::new(p.name.clone(), param_type, true, p.description.clone())
158 })
159 .collect();
160
161 let body_type = request_body_info.as_ref().map(|(bt, _)| {
163 if bt == "any" {
164 "any".to_string()
165 } else if common_schemas.contains(bt) {
166 format!("Common.{}", bt)
167 } else {
168 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
169 format!("{}.{}", namespace_name, bt)
170 }
171 });
172
173 let description = operation
175 .description
176 .clone()
177 .or_else(|| operation.summary.clone())
178 .filter(|s| !s.is_empty())
179 .unwrap_or_default();
180
181 let path_param_names: Vec<String> =
183 path_params_info.iter().map(|p| p.name.clone()).collect();
184 let path_param_names_str = path_param_names.join(", ");
185
186 let mut schema_imports = String::new();
188 let mut needs_common_import = false;
189 let mut needs_namespace_import = false;
190 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
191 let needs_enum_import = !enum_types.is_empty();
192
193 if let Some((body_type, _)) = &request_body_info {
195 if body_type != "any" {
196 if common_schemas.contains(body_type) {
197 needs_common_import = true;
198 } else {
199 needs_namespace_import = true;
200 }
201 }
202 }
203
204 let module_depth = module_name.matches('/').count() + 1; let spec_depth = if spec_name.is_some() { 1 } else { 0 };
210 let total_depth = module_depth + spec_depth + 1; let schemas_depth = total_depth; if needs_common_import {
214 let common_import = if let Some(spec) = spec_name {
215 format!(
216 "{}schemas/{}/common",
217 "../".repeat(schemas_depth),
218 crate::generator::utils::sanitize_module_name(spec)
219 )
220 } else {
221 format!("{}schemas/common", "../".repeat(schemas_depth))
222 };
223 schema_imports.push_str(&format!("import * as Common from \"{}\";", common_import));
224 }
225 if needs_namespace_import {
226 let sanitized_module_name = crate::generator::utils::sanitize_module_name(module_name);
227 let schemas_import = if let Some(spec) = spec_name {
228 format!(
229 "{}schemas/{}/{}",
230 "../".repeat(schemas_depth),
231 crate::generator::utils::sanitize_module_name(spec),
232 sanitized_module_name
233 )
234 } else {
235 format!(
236 "{}schemas/{}",
237 "../".repeat(schemas_depth),
238 sanitized_module_name
239 )
240 };
241 if !schema_imports.is_empty() {
242 schema_imports.push('\n');
243 }
244 schema_imports.push_str(&format!(
245 "import * as {} from \"{}\";",
246 namespace_name, schemas_import
247 ));
248 }
249
250 if needs_enum_import {
252 let sanitized_module_name = crate::generator::utils::sanitize_module_name(module_name);
253 let schemas_import = if let Some(spec) = spec_name {
254 format!(
255 "{}schemas/{}/{}",
256 "../".repeat(schemas_depth),
257 crate::generator::utils::sanitize_module_name(spec),
258 sanitized_module_name
259 )
260 } else {
261 format!(
262 "{}schemas/{}",
263 "../".repeat(schemas_depth),
264 sanitized_module_name
265 )
266 };
267 if !schema_imports.is_empty() {
268 schema_imports.push('\n');
269 }
270 let enum_names: Vec<String> = enum_types.to_vec();
271 schema_imports.push_str(&format!(
272 "import type {{ {} }} from \"{}\";",
273 enum_names.join(", "),
274 schemas_import
275 ));
276 }
277
278 let context = HookContext {
280 hook_name: hook_name.clone(),
281 key_name,
282 operation_id,
283 http_method: op_info.method.clone(),
284 path: op_info.path.clone(),
285 path_params,
286 query_params,
287 body_type,
288 response_type,
289 module_name: module_name.to_string(),
290 spec_name: spec_name.map(|s| s.to_string()),
291 api_import_path: HookContext::calculate_api_import_path(module_name, spec_name),
292 query_keys_import_path: HookContext::calculate_query_keys_import_path(
293 module_name,
294 spec_name,
295 ),
296 param_list,
297 param_names,
298 path_param_names: path_param_names_str,
299 schema_imports,
300 description,
301 };
302
303 let template_id = if is_query {
305 TemplateId::ReactQueryQuery
306 } else {
307 TemplateId::ReactQueryMutation
308 };
309
310 let content = template_engine.render(template_id, &context)?;
311
312 let filename = format!("{}.ts", hook_name);
314
315 hooks.push(HookFile { filename, content });
316 }
317
318 Ok(hooks)
319}
320
321fn generate_hook_name_from_path(path: &str, method: &str) -> String {
323 let path_parts: Vec<&str> = path
324 .trim_start_matches('/')
325 .split('/')
326 .filter(|p| !p.starts_with('{'))
327 .collect();
328
329 let method_upper = method.to_uppercase();
330 let method_prefix = match method_upper.as_str() {
331 "GET" => "get",
332 "POST" => "create",
333 "PUT" => "update",
334 "DELETE" => "delete",
335 "PATCH" => "patch",
336 _ => {
337 return to_camel_case(&method.to_lowercase());
338 }
339 };
340
341 let base_name = if path_parts.is_empty() {
342 method_prefix.to_string()
343 } else {
344 let resource_name = if path_parts.len() > 1 {
345 path_parts.last().unwrap_or(&"")
346 } else {
347 path_parts.first().unwrap_or(&"")
348 };
349
350 if resource_name.ends_with('s') && path.contains('{') {
351 let singular = &resource_name[..resource_name.len() - 1];
352 format!("{}{}ById", method_prefix, to_pascal_case(singular))
353 } else if path.contains('{') {
354 format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
355 } else {
356 format!("{}{}", method_prefix, to_pascal_case(resource_name))
357 }
358 };
359
360 to_camel_case(&base_name)
361}