vika_cli/generator/
query_keys.rs

1use crate::generator::swagger_parser::OperationInfo;
2use crate::generator::utils::to_camel_case;
3use serde::Serialize;
4
5/// Context for query keys generation.
6#[derive(Debug, Clone, Serialize)]
7pub struct QueryKeyContext {
8    pub module_name: String,
9    pub spec_name: Option<String>,
10    pub keys: Vec<QueryKeyEntry>,
11}
12
13/// Entry for a single query key.
14#[derive(Debug, Clone, Serialize)]
15pub struct QueryKeyEntry {
16    pub key_name: String,
17    pub has_params: bool,
18    pub param_list: String, // Full parameter list with types: "id: string, name?: string"
19    pub param_names: String, // Just parameter names: "id, name"
20}
21
22/// Generate query keys context from operations.
23pub fn generate_query_keys(
24    operations: &[OperationInfo],
25    module_name: &str,
26    spec_name: Option<&str>,
27) -> QueryKeyContext {
28    let mut keys = Vec::new();
29
30    for op_info in operations {
31        // Generate key name from operation ID or path/method
32        let key_name = if let Some(operation_id) = &op_info.operation.operation_id {
33            to_camel_case(operation_id)
34        } else {
35            generate_key_name_from_path(&op_info.path, &op_info.method)
36        };
37
38        // Extract parameters for the key
39        let mut params = Vec::new();
40        let mut param_names = Vec::new();
41
42        // Add path parameters
43        for param_ref in &op_info.operation.parameters {
44            if let openapiv3::ReferenceOr::Item(openapiv3::Parameter::Path {
45                parameter_data, ..
46            }) = param_ref
47            {
48                let param_type = extract_param_type(parameter_data);
49                params.push(format!("{}: {}", parameter_data.name, param_type));
50                param_names.push(parameter_data.name.clone());
51            }
52        }
53
54        // Add query parameters as a single object (only for queries, not mutations)
55        let is_query = matches!(op_info.method.to_uppercase().as_str(), "GET" | "HEAD");
56        let mut query_fields = Vec::new();
57        if is_query {
58            for param_ref in &op_info.operation.parameters {
59                if let openapiv3::ReferenceOr::Item(openapiv3::Parameter::Query {
60                    parameter_data,
61                    ..
62                }) = param_ref
63                {
64                    let param_type = extract_param_type(parameter_data);
65                    query_fields.push(format!("{}?: {}", parameter_data.name, param_type));
66                }
67            }
68        }
69
70        // If we have query parameters, add them as a single query object
71        if !query_fields.is_empty() {
72            let query_type = format!("{{ {} }}", query_fields.join(", "));
73            params.push(format!("query?: {}", query_type));
74            param_names.push("query".to_string());
75        }
76
77        let has_params = !params.is_empty();
78        let param_list = params.join(", ");
79        let param_names_str = param_names.join(", ");
80
81        keys.push(QueryKeyEntry {
82            key_name,
83            has_params,
84            param_list,
85            param_names: param_names_str,
86        });
87    }
88
89    QueryKeyContext {
90        module_name: module_name.to_string(),
91        spec_name: spec_name.map(|s| s.to_string()),
92        keys,
93    }
94}
95
96/// Generate key name from path and method (fallback when operation_id is missing).
97fn generate_key_name_from_path(path: &str, method: &str) -> String {
98    let path_parts: Vec<&str> = path
99        .trim_start_matches('/')
100        .split('/')
101        .filter(|p| !p.starts_with('{'))
102        .collect();
103
104    let method_upper = method.to_uppercase();
105    let method_prefix = match method_upper.as_str() {
106        "GET" => "get",
107        "POST" => "create",
108        "PUT" => "update",
109        "DELETE" => "delete",
110        "PATCH" => "patch",
111        _ => {
112            return to_camel_case(&method.to_lowercase());
113        }
114    };
115
116    let base_name = if path_parts.is_empty() {
117        method_prefix.to_string()
118    } else {
119        let resource_name = if path_parts.len() > 1 {
120            path_parts.last().unwrap_or(&"")
121        } else {
122            path_parts.first().unwrap_or(&"")
123        };
124
125        if resource_name.ends_with('s') && path.contains('{') {
126            let singular = &resource_name[..resource_name.len() - 1];
127            format!("{}{}ById", method_prefix, to_pascal_case(singular))
128        } else if path.contains('{') {
129            format!("{}{}ById", method_prefix, to_pascal_case(resource_name))
130        } else {
131            format!("{}{}", method_prefix, to_pascal_case(resource_name))
132        }
133    };
134
135    to_camel_case(&base_name)
136}
137
138/// Extract parameter type from parameter data.
139fn extract_param_type(parameter_data: &openapiv3::ParameterData) -> String {
140    match &parameter_data.format {
141        openapiv3::ParameterSchemaOrContent::Schema(schema_ref) => {
142            match schema_ref {
143                openapiv3::ReferenceOr::Item(schema) => match &schema.schema_kind {
144                    openapiv3::SchemaKind::Type(type_) => match type_ {
145                        openapiv3::Type::String(_) => "string".to_string(),
146                        openapiv3::Type::Number(_) => "number".to_string(),
147                        openapiv3::Type::Integer(_) => "number".to_string(),
148                        openapiv3::Type::Boolean(_) => "boolean".to_string(),
149                        openapiv3::Type::Array(_) => "string[]".to_string(),
150                        openapiv3::Type::Object(_) => "string".to_string(),
151                    },
152                    _ => "string".to_string(),
153                },
154                openapiv3::ReferenceOr::Reference { reference } => {
155                    // Try to extract type name from reference
156                    if let Some(ref_name) = reference.strip_prefix("#/components/schemas/") {
157                        crate::generator::utils::to_pascal_case(ref_name)
158                    } else {
159                        "string".to_string()
160                    }
161                }
162            }
163        }
164        _ => "string".to_string(),
165    }
166}
167
168/// Helper to convert to PascalCase (reuse from utils).
169fn to_pascal_case(s: &str) -> String {
170    crate::generator::utils::to_pascal_case(s)
171}