1use crate::error::{FileSystemError, Result};
2use crate::generator::api_client::ApiFunction;
3use crate::generator::ts_typings::TypeScriptType;
4use crate::generator::utils::sanitize_module_name;
5use crate::generator::zod_schema::ZodSchema;
6use std::collections::hash_map::DefaultHasher;
7use std::hash::{Hash, Hasher};
8use std::path::{Path, PathBuf};
9use std::time::SystemTime;
10
11pub fn ensure_directory(path: &Path) -> Result<()> {
12 if !path.exists() {
13 std::fs::create_dir_all(path).map_err(|e| FileSystemError::CreateDirectoryFailed {
14 path: path.display().to_string(),
15 source: e,
16 })?;
17 }
18 Ok(())
19}
20
21pub fn write_schemas(
22 output_dir: &Path,
23 module_name: &str,
24 types: &[TypeScriptType],
25 zod_schemas: &[ZodSchema],
26) -> Result<Vec<PathBuf>> {
27 write_schemas_with_options(
28 output_dir,
29 module_name,
30 types,
31 zod_schemas,
32 None,
33 false,
34 false,
35 )
36}
37
38pub fn write_schemas_with_options(
39 output_dir: &Path,
40 module_name: &str,
41 types: &[TypeScriptType],
42 zod_schemas: &[ZodSchema],
43 spec_name: Option<&str>,
44 backup: bool,
45 force: bool,
46) -> Result<Vec<PathBuf>> {
47 write_schemas_with_module_mapping(
48 output_dir,
49 module_name,
50 types,
51 zod_schemas,
52 spec_name,
53 backup,
54 force,
55 None, &[], )
58}
59
60#[allow(clippy::too_many_arguments)]
61pub fn write_schemas_with_module_mapping(
62 output_dir: &Path,
63 module_name: &str,
64 types: &[TypeScriptType],
65 zod_schemas: &[ZodSchema],
66 _spec_name: Option<&str>,
67 backup: bool,
68 force: bool,
69 module_schemas: Option<&std::collections::HashMap<String, Vec<String>>>,
70 common_schemas: &[String],
71) -> Result<Vec<PathBuf>> {
72 let module_dir = output_dir.join(sanitize_module_name(module_name));
76 ensure_directory(&module_dir)?;
77
78 let mut written_files = Vec::new();
79
80 if !types.is_empty() {
82 let mut seen_type_names = std::collections::HashSet::new();
85 let mut deduplicated_types = Vec::new();
86 for t in types {
87 let type_name = if let Some(start) = t.content.find("export type ") {
89 let after_export_type = &t.content[start + 12..];
90 if let Some(end) = after_export_type.find([' ', '=', '\n']) {
91 after_export_type[..end].trim().to_string()
92 } else {
93 after_export_type.trim().to_string()
94 }
95 } else if let Some(start) = t.content.find("export interface ") {
96 let after_export_interface = &t.content[start + 17..];
97 if let Some(end) = after_export_interface.find([' ', '{', '\n']) {
98 after_export_interface[..end].trim().to_string()
99 } else {
100 after_export_interface.trim().to_string()
101 }
102 } else {
103 t.content.clone()
105 };
106
107 if !seen_type_names.contains(&type_name) {
108 seen_type_names.insert(type_name);
109 deduplicated_types.push(t);
110 }
111 }
112
113 let types_content_raw = deduplicated_types
114 .iter()
115 .map(|t| t.content.clone())
116 .collect::<Vec<_>>()
117 .join("\n\n");
118
119 let needs_common_import = types_content_raw.contains("Common.");
123 let common_import = if needs_common_import {
124 let relative_path = "../";
128 format!("import * as Common from \"{}common\";\n\n", relative_path)
129 } else {
130 String::new()
131 };
132
133 let types_content =
134 format_typescript_code(&format!("{}{}", common_import, types_content_raw));
135
136 let types_file = module_dir.join("types.ts");
137 write_file_with_backup(&types_file, &types_content, backup, force)?;
138 written_files.push(types_file);
139 }
140
141 if !zod_schemas.is_empty() {
143 let zod_content_raw = zod_schemas
144 .iter()
145 .map(|z| z.content.clone())
146 .collect::<Vec<_>>()
147 .join("\n\n");
148
149 let needs_common_import = zod_content_raw.contains("Common.");
153 let common_import = if needs_common_import {
154 let relative_path = "../";
158 format!("import * as Common from \"{}common\";\n\n", relative_path)
159 } else {
160 String::new()
161 };
162
163 let mut cross_module_imports: std::collections::HashMap<
167 String,
168 std::collections::HashSet<String>,
169 > = std::collections::HashMap::new();
170 if let Some(module_schemas_map) = module_schemas {
171 let _current_module_schemas: std::collections::HashSet<String> = module_schemas_map
172 .get(module_name)
173 .cloned()
174 .unwrap_or_default()
175 .into_iter()
176 .collect();
177
178 let locally_defined_enums: std::collections::HashSet<String> = zod_schemas
180 .iter()
181 .filter_map(|z| {
182 if let Some(start) = z.content.find("export const ") {
184 let after_export = &z.content[start + 13..];
185 if let Some(end) = after_export.find("EnumSchema") {
186 let enum_name = &after_export[..end + "EnumSchema".len()];
187 if enum_name.ends_with("EnumSchema") {
188 return Some(enum_name.to_string());
189 }
190 }
191 }
192 None
193 })
194 .collect();
195
196 let mut pos = 0;
199 while let Some(start) = zod_content_raw[pos..].find("EnumSchema") {
200 let actual_start = pos + start;
201 let mut name_start = actual_start;
203 while name_start > 0 {
204 let ch = zod_content_raw.chars().nth(name_start - 1).unwrap_or(' ');
205 if !ch.is_alphanumeric() && ch != '_' {
206 break;
207 }
208 name_start -= 1;
209 }
210 let enum_name = &zod_content_raw[name_start..actual_start + "EnumSchema".len()];
211
212 if enum_name.starts_with("Common.") {
214 pos = actual_start + "EnumSchema".len();
215 continue;
216 }
217
218 if locally_defined_enums.contains(enum_name) {
220 pos = actual_start + "EnumSchema".len();
221 continue;
222 }
223
224 let schema_name = enum_name.replace("EnumSchema", "");
226
227 if !locally_defined_enums.contains(enum_name)
230 && !common_schemas.contains(&schema_name)
231 {
232 let mut found_module: Option<String> = None;
235 for (other_module, other_schemas) in module_schemas_map {
236 if other_module != module_name && other_schemas.contains(&schema_name) {
237 if !common_schemas.contains(&schema_name) {
240 found_module = Some(other_module.clone());
241 break;
242 }
243 }
244 }
245
246 if found_module.is_none() {
249 let schema_name_lower = schema_name.to_lowercase();
250 for (other_module, other_schemas) in module_schemas_map {
251 if other_module != module_name {
252 for other_schema in other_schemas {
254 let other_schema_lower = other_schema.to_lowercase();
255 if (other_schema_lower == schema_name_lower
258 || other_schema_lower.contains(&schema_name_lower)
259 || schema_name_lower.contains(&other_schema_lower))
260 && !common_schemas.contains(other_schema)
261 {
262 found_module = Some(other_module.clone());
263 break;
264 }
265 }
266 if found_module.is_some() {
267 break;
268 }
269 }
270 }
271 }
272
273 if let Some(module) = found_module {
275 cross_module_imports
276 .entry(module)
277 .or_default()
278 .insert(enum_name.to_string());
279 }
280 }
283
284 pos = actual_start + "EnumSchema".len();
285 }
286 }
287
288 let mut cross_module_import_lines = String::new();
290 for (other_module, enum_names_set) in &cross_module_imports {
291 let mut enum_names: Vec<String> = enum_names_set.iter().cloned().collect();
292 enum_names.sort(); if !enum_names.is_empty() {
294 let relative_path = "../";
295 let module_import = format!(
296 "import {{ {} }} from \"{}{}\";\n",
297 enum_names.join(", "),
298 relative_path,
299 sanitize_module_name(other_module)
300 );
301 cross_module_import_lines.push_str(&module_import);
302 }
303 }
304 if !cross_module_import_lines.is_empty() {
305 cross_module_import_lines.push('\n');
306 }
307
308 let zod_content = format_typescript_code(&format!(
309 "import {{ z }} from \"zod\";\n{}{}{}",
310 if !common_import.is_empty() {
311 &common_import
312 } else {
313 ""
314 },
315 cross_module_import_lines,
316 zod_content_raw
317 ));
318
319 let zod_file = module_dir.join("schemas.ts");
320 write_file_with_backup(&zod_file, &zod_content, backup, force)?;
321 written_files.push(zod_file);
322 }
323
324 let mut index_exports = Vec::new();
326 if !types.is_empty() {
327 index_exports.push("export * from \"./types\";".to_string());
328 }
329 if !zod_schemas.is_empty() {
330 index_exports.push("export * from \"./schemas\";".to_string());
331 }
332
333 if !index_exports.is_empty() {
334 let index_content = format_typescript_code(&(index_exports.join("\n") + "\n"));
338 let index_file = module_dir.join("index.ts");
339 write_file_with_backup(&index_file, &index_content, backup, force)?;
340 written_files.push(index_file);
341 }
342
343 Ok(written_files)
344}
345
346pub fn write_api_client(
347 output_dir: &Path,
348 module_name: &str,
349 functions: &[ApiFunction],
350) -> Result<Vec<PathBuf>> {
351 write_api_client_with_options(output_dir, module_name, functions, None, false, false)
352}
353
354pub fn write_api_client_with_options(
355 output_dir: &Path,
356 module_name: &str,
357 functions: &[ApiFunction],
358 _spec_name: Option<&str>,
359 backup: bool,
360 force: bool,
361) -> Result<Vec<PathBuf>> {
362 let module_dir = output_dir.join(sanitize_module_name(module_name));
366 ensure_directory(&module_dir)?;
367
368 let mut written_files = Vec::new();
369
370 if !functions.is_empty() {
371 let mut imports_by_module: std::collections::HashMap<
375 String,
376 (std::collections::HashSet<String>, Vec<String>),
377 > = std::collections::HashMap::new();
378 let mut function_bodies = Vec::new();
379 let mut seen_functions: std::collections::HashSet<String> =
380 std::collections::HashSet::new();
381
382 for func in functions {
383 let lines: Vec<&str> = func.content.lines().collect();
384 let mut func_lines = Vec::new();
385 let mut type_lines = Vec::new();
386 let mut in_function = false;
387 let mut in_type = false;
388 let mut type_definition = Vec::new();
389 let mut brace_count = 0;
390 let mut jsdoc_lines = Vec::new();
391 let mut in_jsdoc = false;
392 let mut function_name: Option<String> = None;
393
394 for line in lines {
395 if line.trim().starts_with("import ") {
396 let import_line = line.trim().trim_end_matches(';').trim();
397 if let Some(from_pos) = import_line.find(" from ") {
399 let before_from = &import_line[..from_pos];
400 let after_from = &import_line[from_pos + 6..];
401 let module_path = after_from.trim_matches('"').trim_matches('\'').trim();
402
403 if before_from.contains("import type {") {
405 if let Some(start) = before_from.find('{') {
407 if let Some(end) = before_from.find('}') {
408 let items_str = &before_from[start + 1..end];
409 let items: Vec<String> = items_str
410 .split(',')
411 .map(|s| s.trim().to_string())
412 .filter(|s| !s.is_empty())
413 .collect();
414
415 let (type_imports, _) = imports_by_module
416 .entry(module_path.to_string())
417 .or_insert_with(|| {
418 (std::collections::HashSet::new(), Vec::new())
419 });
420 type_imports.extend(items);
421 }
422 }
423 } else if before_from.contains("import * as ") {
424 let (_, other_imports) = imports_by_module
427 .entry(module_path.to_string())
428 .or_insert_with(|| (std::collections::HashSet::new(), Vec::new()));
429 other_imports.push(import_line.to_string());
430 } else {
431 let (_, other_imports) = imports_by_module
434 .entry(module_path.to_string())
435 .or_insert_with(|| (std::collections::HashSet::new(), Vec::new()));
436 other_imports.push(import_line.to_string());
437 }
438 } else {
439 let (_, other_imports) = imports_by_module
441 .entry("".to_string())
442 .or_insert_with(|| (std::collections::HashSet::new(), Vec::new()));
443 other_imports.push(import_line.to_string());
444 }
445 } else if in_type {
446 type_definition.push(line.to_string());
448 brace_count +=
449 line.matches('{').count() as i32 - line.matches('}').count() as i32;
450 if brace_count == 0 && line.trim().ends_with(';') {
451 type_lines.push(type_definition.join("\n"));
453 type_definition.clear();
454 in_type = false;
455 }
456 } else if line.trim().starts_with("/**") {
457 in_jsdoc = true;
459 jsdoc_lines.push(line);
460 } else if in_jsdoc {
461 jsdoc_lines.push(line);
462 if line.trim().ends_with("*/") {
463 in_jsdoc = false;
465 }
466 } else if line.trim().starts_with("export const ") {
467 let trimmed = line.trim();
470 if trimmed.len() > 13 {
471 let after_export_const = &trimmed[13..];
472 let name_end = after_export_const
474 .find(' ')
475 .or_else(|| after_export_const.find('('))
476 .unwrap_or(after_export_const.len());
477 let name = after_export_const[..name_end].trim().to_string();
478 if !name.is_empty() {
479 function_name = Some(name.clone());
480 if seen_functions.contains(&name) {
481 jsdoc_lines.clear();
483 break;
484 }
485 seen_functions.insert(name);
486 }
487 }
488 in_function = true;
489 func_lines.append(&mut jsdoc_lines);
491 func_lines.push(line);
492 } else if in_function {
493 func_lines.push(line);
494 if line.trim() == "};" {
496 break;
497 }
498 } else if line.trim().starts_with("export type ")
499 || line.trim().starts_with("export interface ")
500 {
501 in_type = true;
503 type_definition.clear();
504 type_definition.push(line.to_string());
505 brace_count =
507 line.matches('{').count() as i32 - line.matches('}').count() as i32;
508 if brace_count == 0 && line.trim().ends_with(';') {
509 type_lines.push(type_definition.join("\n"));
511 type_definition.clear();
512 in_type = false;
513 }
514 }
515 }
517
518 if in_type && !type_definition.is_empty() {
520 type_lines.push(type_definition.join("\n"));
521 }
522
523 if !func_lines.is_empty() && function_name.is_some() {
525 let mut combined_content = Vec::new();
526 if !type_lines.is_empty() {
527 combined_content.extend(type_lines.iter().map(|s| s.to_string()));
528 combined_content.push(String::new()); }
530 combined_content.extend(func_lines.iter().map(|s| s.to_string()));
531 function_bodies.push(combined_content.join("\n"));
532 } else if !type_lines.is_empty() {
533 function_bodies.push(
535 type_lines
536 .iter()
537 .map(|s| s.to_string())
538 .collect::<Vec<_>>()
539 .join("\n"),
540 );
541 }
542 }
543
544 let mut sorted_module_paths: Vec<String> = imports_by_module.keys().cloned().collect();
548 sorted_module_paths.sort();
549
550 let mut imports_vec = Vec::new();
551 for module_path in sorted_module_paths {
552 let (type_import_items, other_imports) = imports_by_module.get(&module_path).unwrap();
553 if module_path.is_empty() {
554 let deduped: std::collections::HashSet<String> =
556 other_imports.iter().cloned().collect();
557 imports_vec.extend(deduped.into_iter());
558 } else {
559 let deduped_imports: std::collections::HashSet<String> =
561 other_imports.iter().cloned().collect();
562 let mut namespace_imports = Vec::new();
563 let mut default_imports = Vec::new();
564
565 for item in deduped_imports.iter() {
566 if item.contains("import * as") {
567 namespace_imports.push(item.clone());
569 } else {
570 default_imports.push(item.clone());
572 }
573 }
574
575 namespace_imports.sort();
577 for ns_import in namespace_imports {
578 imports_vec.push(format!("{};", ns_import));
579 }
580
581 default_imports.sort();
583 for default_import in default_imports {
584 imports_vec.push(format!("{};", default_import));
585 }
586
587 if !type_import_items.is_empty() {
589 let mut sorted_types: Vec<String> = type_import_items.iter().cloned().collect();
590 sorted_types.sort();
591 imports_vec.push(format!(
592 "import type {{ {} }} from \"{}\";",
593 sorted_types.join(", "),
594 module_path
595 ));
596 }
597 }
598 }
599 let imports_str = imports_vec.join("\n");
600 let functions_str = function_bodies.join("\n\n");
601 let combined_content = if !imports_str.is_empty() {
602 format!("{}\n\n{}", imports_str, functions_str)
603 } else {
604 functions_str
605 };
606
607 let functions_content = format_typescript_code(&combined_content);
608
609 let api_file = module_dir.join("index.ts");
610 write_file_with_backup(&api_file, &functions_content, backup, force)?;
611 written_files.push(api_file);
612 }
613
614 Ok(written_files)
615}
616
617pub fn write_runtime_client(
619 root_dir: &Path,
620 _spec_name: Option<&str>,
621 apis_config: Option<&crate::config::model::ApisConfig>,
622) -> Result<Vec<PathBuf>> {
623 let runtime_dir = root_dir.join("runtime");
624 ensure_directory(&runtime_dir)?;
625
626 let project_root = std::env::current_dir().ok();
627 let template_engine = crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
628
629 let mut written_files = Vec::new();
630
631 let types_content = template_engine.render(
633 crate::templates::registry::TemplateId::RuntimeTypes,
634 &serde_json::json!({}),
635 )?;
636 let types_file = runtime_dir.join("types.ts");
637 write_file_safe(&types_file, &types_content)?;
638 written_files.push(types_file);
639
640 let mut client_options = serde_json::Map::new();
642 if let Some(config) = apis_config {
643 if let Some(ref base_url) = config.base_url {
644 client_options.insert(
645 "baseUrl".to_string(),
646 serde_json::Value::String(base_url.clone()),
647 );
648 }
649 if let Some(timeout) = config.timeout {
650 client_options.insert(
651 "timeout".to_string(),
652 serde_json::Value::Number(timeout.into()),
653 );
654 }
655 if let Some(retries) = config.retries {
656 client_options.insert(
657 "retries".to_string(),
658 serde_json::Value::Number(retries.into()),
659 );
660 }
661 if let Some(retry_delay) = config.retry_delay {
662 client_options.insert(
663 "retryDelay".to_string(),
664 serde_json::Value::Number(retry_delay.into()),
665 );
666 }
667 if let Some(ref headers) = config.headers {
668 let headers_json: serde_json::Value =
670 serde_json::to_value(headers).unwrap_or(serde_json::json!({}));
671 client_options.insert("headers".to_string(), headers_json);
672 }
673 let auth_strategy = match config.header_strategy.as_str() {
675 "bearerToken" => Some("bearerToken"),
676 "fixed" => Some("fixed"),
677 "consumerInjected" => Some("consumerInjected"),
678 _ => None,
679 };
680 if let Some(auth) = auth_strategy {
681 client_options.insert(
682 "auth".to_string(),
683 serde_json::Value::String(auth.to_string()),
684 );
685 }
686 }
687 let client_options_value = serde_json::Value::Object(client_options);
688
689 let http_client_content = template_engine.render(
691 crate::templates::registry::TemplateId::RuntimeHttpClient,
692 &client_options_value,
693 )?;
694 let http_client_file = runtime_dir.join("http-client.ts");
695 write_file_safe(&http_client_file, &http_client_content)?;
696 written_files.push(http_client_file);
697
698 let index_content = template_engine.render(
700 crate::templates::registry::TemplateId::RuntimeIndex,
701 &serde_json::json!({}),
702 )?;
703 let index_file = runtime_dir.join("index.ts");
704 write_file_safe(&index_file, &index_content)?;
705 written_files.push(index_file);
706
707 Ok(written_files)
708}
709
710fn format_typescript_code(code: &str) -> String {
711 let lines: Vec<&str> = code.lines().collect();
713 let mut formatted = Vec::new();
714 let mut last_was_empty = false;
715
716 for line in lines {
717 if line.trim().is_empty() {
718 if !last_was_empty && !formatted.is_empty() {
719 formatted.push(String::new());
720 last_was_empty = true;
721 }
722 continue;
723 }
724 last_was_empty = false;
725 formatted.push(line.to_string());
726 }
727
728 while formatted.last().map(|s| s.is_empty()).unwrap_or(false) {
730 formatted.pop();
731 }
732
733 formatted.join("\n")
734}
735
736pub fn write_file_safe(path: &Path, content: &str) -> Result<()> {
737 write_file_with_backup(path, content, false, false)
738}
739
740pub fn write_file_with_backup(path: &Path, content: &str, backup: bool, force: bool) -> Result<()> {
741 let file_exists = path.exists();
743 let should_write = if file_exists {
744 if let Ok(existing_content) = std::fs::read_to_string(path) {
745 existing_content != content
746 } else {
747 true
748 }
749 } else {
750 true
751 };
752
753 if !should_write {
754 return Ok(());
756 }
757
758 if backup && file_exists {
760 create_backup(path)?;
761 }
762
763 if !force && file_exists {
765 if let Ok(metadata) = load_file_metadata(path) {
766 let current_hash = compute_content_hash(content);
767 let file_hash = compute_file_hash(path)?;
768
769 if metadata.hash != current_hash && metadata.hash != file_hash {
771 use crate::formatter::FormatterManager;
774
775 let mut search_dir = path.parent().unwrap_or_else(|| Path::new("."));
777 let mut formatter = None;
778
779 while search_dir != Path::new("/") && search_dir != Path::new("") {
781 if let Some(fmt) = FormatterManager::detect_formatter_from_dir(search_dir) {
782 formatter = Some(fmt);
783 break;
784 }
785 if let Some(parent) = search_dir.parent() {
786 search_dir = parent;
787 } else {
788 break;
789 }
790 }
791
792 if formatter.is_none() {
794 formatter = FormatterManager::detect_formatter();
795 }
796
797 if let Some(fmt) = formatter {
798 match FormatterManager::format_content(content, fmt, path) {
800 Ok(formatted_content) => {
801 let formatted_hash = compute_content_hash(&formatted_content);
802 if formatted_hash == file_hash {
803 } else {
806 if current_hash == metadata.hash {
809 return Err(FileSystemError::FileModifiedByUser {
812 path: path.display().to_string(),
813 }
814 .into());
815 }
816 }
819 }
820 Err(_) => {
821 if current_hash == metadata.hash {
823 }
826 }
828 }
829 } else {
830 if current_hash == metadata.hash {
832 }
836 }
838 }
839 }
840 }
841
842 std::fs::write(path, content).map_err(|e| FileSystemError::WriteFileFailed {
844 path: path.display().to_string(),
845 source: e,
846 })?;
847
848 save_file_metadata(path, content)?;
850
851 Ok(())
852}
853
854fn create_backup(path: &Path) -> Result<()> {
855 use std::collections::hash_map::DefaultHasher;
856 use std::hash::{Hash, Hasher};
857 use std::time::{SystemTime, UNIX_EPOCH};
858
859 let timestamp = SystemTime::now()
860 .duration_since(UNIX_EPOCH)
861 .unwrap()
862 .as_secs();
863
864 let backup_dir = PathBuf::from(format!(".vika-backup/{}", timestamp));
865 std::fs::create_dir_all(&backup_dir).map_err(|e| FileSystemError::CreateDirectoryFailed {
866 path: backup_dir.display().to_string(),
867 source: e,
868 })?;
869
870 let backup_path = if path.is_absolute() {
872 let path_str = path.display().to_string();
875 let mut hasher = DefaultHasher::new();
876 path_str.hash(&mut hasher);
877 let hash = format!("{:x}", hasher.finish());
878 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
879 backup_dir.join(format!("{}_{}", hash, filename))
880 } else {
881 let relative_path = path.strip_prefix(".").unwrap_or(path);
883 backup_dir.join(relative_path)
884 };
885
886 if let Some(parent) = backup_path.parent() {
887 std::fs::create_dir_all(parent).map_err(|e| FileSystemError::CreateDirectoryFailed {
888 path: parent.display().to_string(),
889 source: e,
890 })?;
891 }
892
893 std::fs::copy(path, &backup_path).map_err(|e| FileSystemError::WriteFileFailed {
894 path: backup_path.display().to_string(),
895 source: e,
896 })?;
897
898 Ok(())
899}
900
901#[derive(Clone, serde::Serialize, serde::Deserialize)]
902struct FileMetadata {
903 hash: String,
904 generated_at: u64,
905 generated_by: String,
906}
907
908fn compute_content_hash(content: &str) -> String {
909 let mut hasher = DefaultHasher::new();
910 content.hash(&mut hasher);
911 format!("{:x}", hasher.finish())
912}
913
914fn compute_file_hash(path: &Path) -> Result<String> {
915 let content = std::fs::read_to_string(path).map_err(|e| FileSystemError::ReadFileFailed {
916 path: path.display().to_string(),
917 source: e,
918 })?;
919 Ok(compute_content_hash(&content))
920}
921
922pub fn update_file_metadata_from_disk(path: &Path) -> Result<()> {
925 let content = std::fs::read_to_string(path).map_err(|e| FileSystemError::ReadFileFailed {
926 path: path.display().to_string(),
927 source: e,
928 })?;
929 save_file_metadata(path, &content)
930}
931
932pub fn batch_update_file_metadata_from_disk(paths: &[PathBuf]) -> Result<()> {
936 if paths.is_empty() {
937 return Ok(());
938 }
939
940 let metadata_dir = PathBuf::from(".vika-cache");
941 std::fs::create_dir_all(&metadata_dir).map_err(|e| FileSystemError::CreateDirectoryFailed {
942 path: metadata_dir.display().to_string(),
943 source: e,
944 })?;
945
946 let metadata_file = metadata_dir.join("file-metadata.json");
947 let mut metadata_map: std::collections::HashMap<String, FileMetadata> =
948 if metadata_file.exists() {
949 let content = std::fs::read_to_string(&metadata_file).map_err(|e| {
950 FileSystemError::ReadFileFailed {
951 path: metadata_file.display().to_string(),
952 source: e,
953 }
954 })?;
955 serde_json::from_str(&content).unwrap_or_default()
956 } else {
957 std::collections::HashMap::new()
958 };
959
960 let generated_at = SystemTime::now()
961 .duration_since(std::time::UNIX_EPOCH)
962 .unwrap()
963 .as_secs();
964
965 for path in paths {
967 match std::fs::read_to_string(path) {
968 Ok(content) => {
969 let hash = compute_content_hash(&content);
970 metadata_map.insert(
971 path.display().to_string(),
972 FileMetadata {
973 hash,
974 generated_at,
975 generated_by: "vika-cli".to_string(),
976 },
977 );
978 }
979 Err(e) => {
980 eprintln!("Warning: Failed to read {}: {}", path.display(), e);
982 }
983 }
984 }
985
986 let json = serde_json::to_string_pretty(&metadata_map).map_err(|e| {
988 FileSystemError::WriteFileFailed {
989 path: metadata_file.display().to_string(),
990 source: std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{}", e)),
991 }
992 })?;
993
994 std::fs::write(&metadata_file, json).map_err(|e| FileSystemError::WriteFileFailed {
995 path: metadata_file.display().to_string(),
996 source: e,
997 })?;
998
999 Ok(())
1000}
1001
1002pub fn save_file_metadata(path: &Path, content: &str) -> Result<()> {
1003 let metadata_dir = PathBuf::from(".vika-cache");
1004 std::fs::create_dir_all(&metadata_dir).map_err(|e| FileSystemError::CreateDirectoryFailed {
1005 path: metadata_dir.display().to_string(),
1006 source: e,
1007 })?;
1008
1009 let metadata_file = metadata_dir.join("file-metadata.json");
1010 let mut metadata_map: std::collections::HashMap<String, FileMetadata> =
1011 if metadata_file.exists() {
1012 let content = std::fs::read_to_string(&metadata_file).map_err(|e| {
1013 FileSystemError::ReadFileFailed {
1014 path: metadata_file.display().to_string(),
1015 source: e,
1016 }
1017 })?;
1018 serde_json::from_str(&content).unwrap_or_default()
1019 } else {
1020 std::collections::HashMap::new()
1021 };
1022
1023 let hash = compute_content_hash(content);
1024 let generated_at = SystemTime::now()
1025 .duration_since(std::time::UNIX_EPOCH)
1026 .unwrap()
1027 .as_secs();
1028
1029 metadata_map.insert(
1030 path.display().to_string(),
1031 FileMetadata {
1032 hash,
1033 generated_at,
1034 generated_by: "vika-cli".to_string(),
1035 },
1036 );
1037
1038 let json = serde_json::to_string_pretty(&metadata_map).map_err(|e| {
1039 FileSystemError::WriteFileFailed {
1040 path: metadata_file.display().to_string(),
1041 source: std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{}", e)),
1042 }
1043 })?;
1044
1045 std::fs::write(&metadata_file, json).map_err(|e| FileSystemError::WriteFileFailed {
1046 path: metadata_file.display().to_string(),
1047 source: e,
1048 })?;
1049
1050 Ok(())
1051}
1052
1053fn load_file_metadata(path: &Path) -> Result<FileMetadata> {
1054 let metadata_file = PathBuf::from(".vika-cache/file-metadata.json");
1055 if !metadata_file.exists() {
1056 return Err(FileSystemError::FileNotFound {
1057 path: metadata_file.display().to_string(),
1058 }
1059 .into());
1060 }
1061
1062 let content =
1063 std::fs::read_to_string(&metadata_file).map_err(|e| FileSystemError::ReadFileFailed {
1064 path: metadata_file.display().to_string(),
1065 source: e,
1066 })?;
1067
1068 let metadata_map: std::collections::HashMap<String, FileMetadata> =
1069 serde_json::from_str(&content).map_err(|e| FileSystemError::ReadFileFailed {
1070 path: metadata_file.display().to_string(),
1071 source: std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{}", e)),
1072 })?;
1073
1074 metadata_map
1075 .get(&path.display().to_string())
1076 .cloned()
1077 .ok_or_else(|| {
1078 FileSystemError::FileNotFound {
1079 path: path.display().to_string(),
1080 }
1081 .into()
1082 })
1083}
1084
1085pub fn write_hooks_with_options(
1087 output_dir: &Path,
1088 module_name: &str,
1089 hooks: &[crate::generator::hooks::HookFile],
1090 _spec_name: Option<&str>,
1091 backup: bool,
1092 force: bool,
1093) -> Result<Vec<PathBuf>> {
1094 let module_dir = output_dir.join(sanitize_module_name(module_name));
1097 ensure_directory(&module_dir)?;
1098
1099 let mut written_files = Vec::new();
1100
1101 for hook in hooks {
1102 let hook_file = module_dir.join(&hook.filename);
1103 write_file_with_backup(&hook_file, &hook.content, backup, force)?;
1104 written_files.push(hook_file);
1105 }
1106
1107 Ok(written_files)
1108}
1109
1110pub fn write_query_keys_with_options(
1112 output_dir: &Path,
1113 module_name: &str,
1114 query_keys_content: &str,
1115 _spec_name: Option<&str>,
1116 backup: bool,
1117 force: bool,
1118) -> Result<PathBuf> {
1119 ensure_directory(output_dir)?;
1122
1123 let filename = format!("{}.ts", sanitize_module_name(module_name));
1125 let query_keys_file = output_dir.join(&filename);
1126
1127 write_file_with_backup(&query_keys_file, query_keys_content, backup, force)?;
1128
1129 Ok(query_keys_file)
1130}