vika_cli/generator/hooks/
context.rs

1use crate::generator::utils::sanitize_module_name;
2use crate::templates::context::Parameter;
3use serde::Serialize;
4
5/// Context for hook generation.
6#[derive(Debug, Clone, Serialize)]
7pub struct HookContext {
8    pub hook_name: String,
9    pub key_name: String,
10    pub operation_id: String,
11    pub http_method: String,
12    pub path: String,
13    pub path_params: Vec<Parameter>,
14    pub query_params: Vec<Parameter>,
15    pub body_type: Option<String>,
16    pub response_type: String,
17    pub module_name: String,
18    pub spec_name: Option<String>,
19    pub api_import_path: String,
20    pub query_keys_import_path: String,
21    pub param_list: String, // Full parameter list with types: "id: string, query?: { page?: number }"
22    pub param_names: String, // Just parameter names for function calls: "id, query"
23    pub path_param_names: String, // Just path parameter names: "id"
24    pub schema_imports: String, // Schema import statements
25    pub description: String,
26    pub success_map_type: String, // e.g., "OrdersControllerCreateResponses"
27    pub error_map_type: String,   // e.g., "OrdersControllerCreateErrors"
28    pub generic_result_type: String, // e.g., "ApiResult<OrdersControllerCreateResponses, OrdersControllerCreateErrors>"
29    pub import_runtime_path: String, // Path to runtime types/client
30}
31
32impl HookContext {
33    /// Calculate import path to API functions.
34    /// From: src/hooks/{module}/useX.ts
35    /// To: {apis_dir}/{module}/index.ts
36    /// Note: apis_dir doesn't include spec_name (it's in config if needed), just like schemas
37    pub fn calculate_api_import_path(
38        module_name: &str,
39        _spec_name: Option<&str>,
40        apis_dir: Option<&str>,
41    ) -> String {
42        let module_depth = module_name.matches('/').count() + 1; // +1 for module directory
43        let hooks_depth = 1; // hooks directory
44        let total_depth = module_depth + hooks_depth;
45
46        if let Some(apis) = apis_dir {
47            // Calculate relative path from hooks/{module}/ to {apis_dir}/{module}/
48            // Note: hooks_dir and apis_dir don't include spec_name (it's in config if needed)
49            let hooks_path = format!("src/hooks/{}", module_name);
50
51            let common_prefix = HookContext::find_common_prefix(&hooks_path, apis);
52            let hooks_relative = hooks_path
53                .strip_prefix(&common_prefix)
54                .unwrap_or(&hooks_path)
55                .trim_start_matches('/');
56            let apis_relative = apis
57                .strip_prefix(&common_prefix)
58                .unwrap_or(apis)
59                .trim_start_matches('/');
60
61            let hooks_depth_from_common = if hooks_relative.is_empty() {
62                0
63            } else {
64                hooks_relative.matches('/').count() + 1
65            };
66
67            let sanitized_module = sanitize_module_name(module_name);
68            if apis_relative.is_empty() {
69                format!(
70                    "{}{}",
71                    "../".repeat(hooks_depth_from_common),
72                    sanitized_module
73                )
74            } else {
75                format!(
76                    "{}{}/{}",
77                    "../".repeat(hooks_depth_from_common),
78                    apis_relative,
79                    sanitized_module
80                )
81            }
82        } else {
83            // Fallback: assume apis is at src/apis/{module}
84            let sanitized_module = sanitize_module_name(module_name);
85            format!("{}apis/{}", "../".repeat(total_depth), sanitized_module)
86        }
87    }
88
89    /// Find the common prefix of two paths
90    pub fn find_common_prefix(path1: &str, path2: &str) -> String {
91        let parts1: Vec<&str> = path1.split('/').collect();
92        let parts2: Vec<&str> = path2.split('/').collect();
93
94        let mut common = Vec::new();
95        let min_len = parts1.len().min(parts2.len());
96
97        for i in 0..min_len {
98            if parts1[i] == parts2[i] {
99                common.push(parts1[i]);
100            } else {
101                break;
102            }
103        }
104
105        common.join("/")
106    }
107
108    /// Calculate import path to query keys.
109    /// From: {hooks_dir}/{module}/useX.ts
110    /// To: {query_keys_dir}/{module}.ts
111    /// Note: output_dir already includes spec_name if needed (from config), just like schemas/apis
112    pub fn calculate_query_keys_import_path(
113        module_name: &str,
114        _spec_name: Option<&str>,
115        hooks_dir: Option<&str>,
116        query_keys_dir: Option<&str>,
117    ) -> String {
118        let module_depth = module_name.matches('/').count() + 1; // +1 for module directory
119        let hooks_depth = 1; // hooks directory
120        let total_depth = module_depth + hooks_depth;
121
122        if let (Some(hooks), Some(query_keys)) = (hooks_dir, query_keys_dir) {
123            // Calculate relative path from hooks/{module}/ to {query_keys_dir}/{module}.ts
124            // Note: hooks_dir and query_keys_dir don't include spec_name (it's in config if needed)
125            let hooks_path = format!("{}/{}", hooks, module_name);
126
127            let common_prefix = HookContext::find_common_prefix(&hooks_path, query_keys);
128            let hooks_relative = hooks_path
129                .strip_prefix(&common_prefix)
130                .unwrap_or(&hooks_path)
131                .trim_start_matches('/');
132            let query_keys_relative = query_keys
133                .strip_prefix(&common_prefix)
134                .unwrap_or(query_keys)
135                .trim_start_matches('/');
136
137            let hooks_depth_from_common = if hooks_relative.is_empty() {
138                0
139            } else {
140                hooks_relative.matches('/').count() + 1
141            };
142
143            let sanitized_module = sanitize_module_name(module_name);
144            if query_keys_relative.is_empty() {
145                format!(
146                    "{}{}",
147                    "../".repeat(hooks_depth_from_common),
148                    sanitized_module
149                )
150            } else {
151                format!(
152                    "{}{}/{}",
153                    "../".repeat(hooks_depth_from_common),
154                    query_keys_relative,
155                    sanitized_module
156                )
157            }
158        } else {
159            // Fallback: assume query-keys is at src/query-keys/{module}
160            let sanitized_module = sanitize_module_name(module_name);
161            format!(
162                "{}query-keys/{}",
163                "../".repeat(total_depth),
164                sanitized_module
165            )
166        }
167    }
168
169    /// Calculate import path to runtime client.
170    /// From: src/hooks/{module}/useX.ts
171    /// To: src/runtime/index.ts
172    /// Note: hooks_dir doesn't include spec_name (it's in config if needed), just like schemas/apis
173    pub fn calculate_runtime_import_path(module_name: &str, _spec_name: Option<&str>) -> String {
174        // Calculate depth: hooks/{module}/ -> runtime/
175        // Example: hooks/addresses/ -> ../../runtime
176        let module_depth = module_name.matches('/').count() + 1; // +1 for module directory
177        let hooks_depth = 1; // hooks directory
178        let total_depth = module_depth + hooks_depth;
179
180        format!("{}runtime", "../".repeat(total_depth))
181    }
182}