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
15#[allow(clippy::too_many_arguments)]
17pub fn generate_react_query_hooks(
18 openapi: &OpenAPI,
19 operations: &[OperationInfo],
20 module_name: &str,
21 spec_name: Option<&str>,
22 common_schemas: &[String],
23 enum_registry: &mut std::collections::HashMap<String, String>,
24 template_engine: &TemplateEngine,
25 apis_dir: Option<&str>,
26 schemas_dir: Option<&str>,
27 hooks_dir: Option<&str>,
28 query_keys_dir: Option<&str>,
29) -> Result<Vec<HookFile>> {
30 let mut hooks = Vec::new();
31
32 for op_info in operations {
33 let operation = &op_info.operation;
34 let method = op_info.method.to_uppercase();
35
36 let is_query = matches!(method.as_str(), "GET" | "HEAD");
38 let is_mutation = matches!(method.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
39
40 if !is_query && !is_mutation {
41 continue; }
43
44 let operation_id = if let Some(op_id) = &operation.operation_id {
46 to_camel_case(op_id)
47 } else {
48 generate_hook_name_from_path(&op_info.path, &op_info.method)
49 };
50
51 let hook_name = format!("use{}", to_pascal_case(&operation_id));
53
54 let key_name = operation_id.clone();
56
57 let path_params_info = extract_path_parameters(openapi, operation, enum_registry)?;
59 let query_params_info = extract_query_parameters(openapi, operation, enum_registry)?;
60 let request_body_info = extract_request_body(openapi, operation)?;
61 let all_responses = extract_all_responses(openapi, operation)?;
62
63 let success_responses: Vec<ResponseInfo> = all_responses
65 .iter()
66 .filter(|r| r.status_code >= 200 && r.status_code < 300)
67 .cloned()
68 .collect();
69 let _error_responses: Vec<ResponseInfo> = all_responses
70 .iter()
71 .filter(|r| r.status_code < 200 || r.status_code >= 300)
72 .cloned()
73 .collect();
74 let response_type = success_responses
75 .iter()
76 .find(|r| r.status_code == 200)
77 .map(|r| r.body_type.clone())
78 .unwrap_or_else(|| "any".to_string());
79
80 let type_name_base = to_pascal_case(&operation_id);
82 let success_map_type = format!("{}Responses", type_name_base);
83 let error_map_type = format!("{}Errors", type_name_base);
84 let generic_result_type = format!("ApiResult<{}, {}>", success_map_type, error_map_type);
85
86 let mut param_list_parts = Vec::new();
88 let mut param_names_parts = Vec::new();
89
90 for param in &path_params_info {
92 let param_type = match ¶m.param_type {
93 crate::generator::api_client::ParameterType::Enum(enum_name) => enum_name.clone(),
94 crate::generator::api_client::ParameterType::String => "string".to_string(),
95 crate::generator::api_client::ParameterType::Number => "number".to_string(),
96 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
97 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
98 crate::generator::api_client::ParameterType::Array(_) => "string".to_string(),
99 };
100 param_list_parts.push(format!("{}: {}", param.name, param_type));
101 param_names_parts.push(param.name.clone());
102 }
103
104 let mut enum_types = Vec::new();
106 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
107
108 if is_query && !query_params_info.is_empty() {
111 let mut query_fields = Vec::new();
112 for param in &query_params_info {
113 let param_type = match ¶m.param_type {
114 crate::generator::api_client::ParameterType::Enum(enum_name) => {
115 enum_types.push(enum_name.clone());
116 format!("{}.{}", namespace_name, enum_name)
118 }
119 crate::generator::api_client::ParameterType::Array(item_type) => {
120 format!("{}[]", item_type)
121 }
122 crate::generator::api_client::ParameterType::String => "string".to_string(),
123 crate::generator::api_client::ParameterType::Number => "number".to_string(),
124 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
125 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
126 };
127 query_fields.push(format!("{}?: {}", param.name, param_type));
128 }
129 let query_type = format!("{{ {} }}", query_fields.join(", "));
130 param_list_parts.push(format!("query?: {}", query_type));
131 param_names_parts.push("query".to_string());
132 }
133
134 let param_list = param_list_parts.join(", ");
139 let param_names = param_names_parts.join(", ");
140
141 let path_params: Vec<ApiParameter> = path_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, false, p.description.clone())
158 })
159 .collect();
160
161 let query_params: Vec<ApiParameter> = query_params_info
162 .iter()
163 .map(|p| {
164 let param_type = match &p.param_type {
165 crate::generator::api_client::ParameterType::Enum(enum_name) => {
166 enum_name.clone()
167 }
168 crate::generator::api_client::ParameterType::Array(item_type) => {
169 format!("{}[]", item_type)
170 }
171 crate::generator::api_client::ParameterType::String => "string".to_string(),
172 crate::generator::api_client::ParameterType::Number => "number".to_string(),
173 crate::generator::api_client::ParameterType::Integer => "number".to_string(),
174 crate::generator::api_client::ParameterType::Boolean => "boolean".to_string(),
175 };
176 ApiParameter::new(p.name.clone(), param_type, true, p.description.clone())
177 })
178 .collect();
179
180 let body_type = request_body_info.as_ref().map(|(bt, _)| {
182 if bt == "any" {
183 "any".to_string()
184 } else if common_schemas.contains(bt) {
185 format!("Common.{}", bt)
186 } else {
187 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
188 format!("{}.{}", namespace_name, bt)
189 }
190 });
191
192 let description = operation
194 .description
195 .clone()
196 .or_else(|| operation.summary.clone())
197 .filter(|s| !s.is_empty())
198 .unwrap_or_default();
199
200 let path_param_names: Vec<String> =
202 path_params_info.iter().map(|p| p.name.clone()).collect();
203 let path_param_names_str = path_param_names.join(", ");
204
205 let mut schema_imports = String::new();
207 let mut needs_common_import = false;
208 let mut needs_namespace_import = false;
209 let namespace_name = to_pascal_case(&module_name.replace("/", "_"));
210 let needs_enum_import = !enum_types.is_empty();
211
212 if let Some((body_type, _)) = &request_body_info {
214 if body_type != "any" {
215 if common_schemas.contains(body_type) {
216 needs_common_import = true;
217 } else {
218 needs_namespace_import = true;
219 }
220 }
221 }
222
223 let module_depth = module_name.matches('/').count() + 1; let hooks_depth = 1; let total_depth = module_depth + hooks_depth;
230
231 let schemas_import_base = if let Some(schemas) = schemas_dir {
232 let hooks_path = format!("src/hooks/{}", module_name);
234
235 let common_prefix = HookContext::find_common_prefix(&hooks_path, schemas);
236 let hooks_relative = hooks_path
237 .strip_prefix(&common_prefix)
238 .unwrap_or(&hooks_path)
239 .trim_start_matches('/');
240 let schemas_relative = schemas
241 .strip_prefix(&common_prefix)
242 .unwrap_or(schemas)
243 .trim_start_matches('/');
244
245 let hooks_depth_from_common = if hooks_relative.is_empty() {
246 0
247 } else {
248 hooks_relative.matches('/').count() + 1
249 };
250
251 if schemas_relative.is_empty() {
252 "../".repeat(hooks_depth_from_common)
253 } else {
254 format!(
255 "{}{}",
256 "../".repeat(hooks_depth_from_common),
257 schemas_relative
258 )
259 }
260 } else {
261 format!("{}schemas", "../".repeat(total_depth))
263 };
264
265 let schemas_dir_includes_spec =
267 if let (Some(schemas), Some(spec)) = (schemas_dir, spec_name) {
268 let schemas_normalized = schemas.trim_end_matches('/');
269 let spec_normalized = crate::generator::utils::sanitize_module_name(spec);
270 schemas_normalized.ends_with(&spec_normalized)
271 || schemas_normalized.ends_with(&format!("/{}", spec_normalized))
272 } else {
273 false
274 };
275
276 if needs_common_import {
277 let common_import = if schemas_dir_includes_spec {
278 format!("{}/common", schemas_import_base.trim_end_matches('/'))
280 } else {
281 format!("{}/common", schemas_import_base.trim_end_matches('/'))
284 };
285 schema_imports.push_str(&format!("import * as Common from \"{}\";", common_import));
286 }
287 if needs_namespace_import {
288 let sanitized_module_name = crate::generator::utils::sanitize_module_name(module_name);
289 let schemas_import = if schemas_dir_includes_spec {
290 format!(
292 "{}/{}",
293 schemas_import_base.trim_end_matches('/'),
294 sanitized_module_name
295 )
296 } else {
297 format!(
300 "{}/{}",
301 schemas_import_base.trim_end_matches('/'),
302 sanitized_module_name
303 )
304 };
305 if !schema_imports.is_empty() {
306 schema_imports.push('\n');
307 }
308 schema_imports.push_str(&format!(
309 "import * as {} from \"{}\";",
310 namespace_name, schemas_import
311 ));
312 }
313
314 if needs_enum_import {
317 if !needs_namespace_import {
319 let sanitized_module_name =
320 crate::generator::utils::sanitize_module_name(module_name);
321 let schemas_import = if schemas_dir_includes_spec {
322 format!(
323 "{}/{}",
324 schemas_import_base.trim_end_matches('/'),
325 sanitized_module_name
326 )
327 } else {
328 format!(
330 "{}/{}",
331 schemas_import_base.trim_end_matches('/'),
332 sanitized_module_name
333 )
334 };
335 if !schema_imports.is_empty() {
336 schema_imports.push('\n');
337 }
338 schema_imports.push_str(&format!(
339 "import * as {} from \"{}\";",
340 namespace_name, schemas_import
341 ));
342 }
343 }
346
347 let context = HookContext {
349 hook_name: hook_name.clone(),
350 key_name,
351 operation_id,
352 http_method: op_info.method.clone(),
353 path: op_info.path.clone(),
354 path_params,
355 query_params,
356 body_type,
357 response_type,
358 module_name: module_name.to_string(),
359 spec_name: spec_name.map(|s| s.to_string()),
360 api_import_path: HookContext::calculate_api_import_path(
361 module_name,
362 spec_name,
363 apis_dir,
364 ),
365 query_keys_import_path: HookContext::calculate_query_keys_import_path(
366 module_name,
367 spec_name,
368 hooks_dir,
369 query_keys_dir,
370 ),
371 param_list,
372 param_names,
373 path_param_names: path_param_names_str,
374 schema_imports,
375 description,
376 success_map_type,
377 error_map_type,
378 generic_result_type,
379 import_runtime_path: HookContext::calculate_runtime_import_path(module_name, spec_name),
380 };
381
382 let template_id = if is_query {
384 TemplateId::ReactQueryQuery
385 } else {
386 TemplateId::ReactQueryMutation
387 };
388
389 let content = template_engine.render(template_id, &context)?;
390
391 let filename = format!("{}.ts", hook_name);
393
394 hooks.push(HookFile { filename, content });
395 }
396
397 Ok(hooks)
398}
399
400fn generate_hook_name_from_path(path: &str, method: &str) -> String {
402 let path_parts: Vec<&str> = path
403 .trim_start_matches('/')
404 .split('/')
405 .filter(|p| !p.starts_with('{'))
406 .collect();
407
408 let method_upper = method.to_uppercase();
409 let method_prefix = match method_upper.as_str() {
410 "GET" => "get",
411 "POST" => "create",
412 "PUT" => "update",
413 "DELETE" => "delete",
414 "PATCH" => "patch",
415 _ => {
416 return to_camel_case(&method.to_lowercase());
417 }
418 };
419
420 let base_name = if path_parts.is_empty() {
421 method_prefix.to_string()
422 } else {
423 let resource_name = if path_parts.len() > 1 {
424 path_parts.last().unwrap_or(&"")
425 } else {
426 path_parts.first().unwrap_or(&"")
427 };
428
429 if resource_name.ends_with('s') && path.contains('{') {
430 let singular = &resource_name[..resource_name.len() - 1];
431 format!("{}{}ById", method_prefix, to_pascal_case(singular))
432 } else if path.contains('{') {
433 format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
434 } else {
435 format!("{}{}", method_prefix, to_pascal_case(resource_name))
436 }
437 };
438
439 to_camel_case(&base_name)
440}