1use std::path::Path;
2
3use crate::core::cache::SessionCache;
4use crate::core::compressor;
5use crate::core::deps;
6use crate::core::entropy;
7use crate::core::protocol;
8use crate::core::signatures;
9use crate::core::symbol_map::{self, SymbolMap};
10use crate::core::tokens::count_tokens;
11use crate::tools::CrpMode;
12
13pub fn read_file_lossy(path: &str) -> Result<String, std::io::Error> {
14 let bytes = std::fs::read(path)?;
15 match String::from_utf8(bytes) {
16 Ok(s) => Ok(s),
17 Err(e) => Ok(String::from_utf8_lossy(e.as_bytes()).into_owned()),
18 }
19}
20
21pub fn handle(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
22 handle_with_options(cache, path, mode, false, crp_mode, None)
23}
24
25pub fn handle_fresh(cache: &mut SessionCache, path: &str, mode: &str, crp_mode: CrpMode) -> String {
26 handle_with_options(cache, path, mode, true, crp_mode, None)
27}
28
29pub fn handle_with_task(
30 cache: &mut SessionCache,
31 path: &str,
32 mode: &str,
33 crp_mode: CrpMode,
34 task: Option<&str>,
35) -> String {
36 handle_with_options(cache, path, mode, false, crp_mode, task)
37}
38
39pub fn handle_fresh_with_task(
40 cache: &mut SessionCache,
41 path: &str,
42 mode: &str,
43 crp_mode: CrpMode,
44 task: Option<&str>,
45) -> String {
46 handle_with_options(cache, path, mode, true, crp_mode, task)
47}
48
49fn handle_with_options(
50 cache: &mut SessionCache,
51 path: &str,
52 mode: &str,
53 fresh: bool,
54 crp_mode: CrpMode,
55 task: Option<&str>,
56) -> String {
57 let file_ref = cache.get_file_ref(path);
58 let short = protocol::shorten_path(path);
59 let ext = Path::new(path)
60 .extension()
61 .and_then(|e| e.to_str())
62 .unwrap_or("");
63
64 if fresh {
65 cache.invalidate(path);
66 }
67
68 if mode == "diff" {
69 return handle_diff(cache, path, &file_ref);
70 }
71
72 if cache.get(path).is_some() {
73 if mode == "full" {
74 let result = handle_full_with_auto_delta(cache, path, &file_ref, &short, ext, crp_mode);
75 return maybe_apply_task_filter(result, cache, path, task);
76 }
77 let existing = cache.get(path).unwrap();
78 let content = existing.content.clone();
79 let original_tokens = existing.original_tokens;
80 return process_mode(
81 &content,
82 mode,
83 &file_ref,
84 &short,
85 ext,
86 original_tokens,
87 crp_mode,
88 path,
89 task,
90 );
91 }
92
93 let content = match read_file_lossy(path) {
94 Ok(c) => c,
95 Err(e) => return format!("ERROR: {e}"),
96 };
97
98 let (entry, _is_hit) = cache.store(path, content.clone());
99
100 if mode == "full" {
101 let result = format_full_output(cache, &file_ref, &short, ext, &content, &entry, crp_mode);
102 return maybe_apply_task_filter(result, cache, path, task);
103 }
104
105 process_mode(
106 &content,
107 mode,
108 &file_ref,
109 &short,
110 ext,
111 entry.original_tokens,
112 crp_mode,
113 path,
114 task,
115 )
116}
117
118const AUTO_DELTA_THRESHOLD: f64 = 0.6;
119
120fn handle_full_with_auto_delta(
122 cache: &mut SessionCache,
123 path: &str,
124 file_ref: &str,
125 short: &str,
126 ext: &str,
127 crp_mode: CrpMode,
128) -> String {
129 let disk_content = match read_file_lossy(path) {
130 Ok(c) => c,
131 Err(_) => {
132 cache.record_cache_hit(path);
133 let existing = cache.get(path).unwrap();
134 return format!(
135 "{file_ref}={short} cached {}t {}L",
136 existing.read_count, existing.line_count
137 );
138 }
139 };
140
141 let old_content = cache.get(path).unwrap().content.clone();
142 let (entry, is_hit) = cache.store(path, disk_content.clone());
143
144 if is_hit {
145 return format!(
146 "{file_ref}={short} cached {}t {}L",
147 entry.read_count, entry.line_count
148 );
149 }
150
151 let diff = compressor::diff_content(&old_content, &disk_content);
152 let diff_tokens = count_tokens(&diff);
153 let full_tokens = entry.original_tokens;
154
155 if full_tokens > 0 && (diff_tokens as f64) < (full_tokens as f64 * AUTO_DELTA_THRESHOLD) {
156 let savings = protocol::format_savings(full_tokens, diff_tokens);
157 return format!(
158 "{file_ref}={short} [auto-delta] ∆{}L\n{diff}\n{savings}",
159 disk_content.lines().count()
160 );
161 }
162
163 format_full_output(cache, file_ref, short, ext, &disk_content, &entry, crp_mode)
164}
165
166fn format_full_output(
167 _cache: &mut SessionCache,
168 file_ref: &str,
169 short: &str,
170 ext: &str,
171 content: &str,
172 entry: &crate::core::cache::CacheEntry,
173 _crp_mode: CrpMode,
174) -> String {
175 let tokens = entry.original_tokens;
176 let metadata = build_header(file_ref, short, ext, content, entry.line_count, true);
177
178 let mut sym = SymbolMap::new();
179 let idents = symbol_map::extract_identifiers(content, ext);
180 for ident in &idents {
181 sym.register(ident);
182 }
183
184 let sym_beneficial = if sym.len() >= 3 {
185 let sym_table = sym.format_table();
186 let compressed = sym.apply(content);
187 let original_tok = count_tokens(content);
188 let compressed_tok = count_tokens(&compressed) + count_tokens(&sym_table);
189 let net_saving = original_tok.saturating_sub(compressed_tok);
190 original_tok > 0 && net_saving * 100 / original_tok >= 5
191 } else {
192 false
193 };
194
195 if sym_beneficial {
196 let compressed_content = sym.apply(content);
197 let sym_table = sym.format_table();
198 let output = format!("{compressed_content}{sym_table}\n{metadata}");
199 let sent = count_tokens(&output);
200 let savings = protocol::format_savings(tokens, sent);
201 return format!("{output}\n{savings}");
202 }
203
204 let output = format!("{content}\n{metadata}");
205 let sent = count_tokens(&output);
206 let savings = protocol::format_savings(tokens, sent);
207 format!("{output}\n{savings}")
208}
209
210const TASK_FILTER_TOKEN_THRESHOLD: usize = 1000;
211const TASK_FILTER_BUDGET_RATIO: f64 = 0.5;
212
213fn maybe_apply_task_filter(
214 full_output: String,
215 cache: &mut SessionCache,
216 path: &str,
217 task: Option<&str>,
218) -> String {
219 let task_str = match task {
220 Some(t) if !t.is_empty() => t,
221 _ => return full_output,
222 };
223
224 let ext = Path::new(path)
225 .extension()
226 .and_then(|e| e.to_str())
227 .unwrap_or("");
228
229 if !crate::tools::ctx_smart_read::is_code_ext(ext) {
230 return full_output;
231 }
232
233 let original_tokens = match cache.get(path) {
234 Some(entry) => entry.original_tokens,
235 None => return full_output,
236 };
237
238 if original_tokens < TASK_FILTER_TOKEN_THRESHOLD {
239 return full_output;
240 }
241
242 let content = match cache.get(path) {
243 Some(entry) => entry.content.clone(),
244 None => return full_output,
245 };
246
247 let (_files, keywords) = crate::core::task_relevance::parse_task_hints(task_str);
248 if keywords.is_empty() {
249 return full_output;
250 }
251
252 let original_lines = content.lines().count();
253 let filtered = crate::core::task_relevance::information_bottleneck_filter(
254 &content,
255 &keywords,
256 TASK_FILTER_BUDGET_RATIO,
257 );
258 let filtered_lines = filtered.lines().count();
259
260 if filtered_lines >= original_lines {
261 return full_output;
262 }
263
264 let file_ref = cache.get_file_ref(path);
265 let short = protocol::shorten_path(path);
266 let header = format!(
267 "{file_ref}={short} {original_lines}L [task-enhanced: {original_lines}→{filtered_lines}]"
268 );
269 let sent = count_tokens(&filtered) + count_tokens(&header);
270 let savings = protocol::format_savings(original_tokens, sent);
271 format!("{header}\n{filtered}\n{savings}")
272}
273
274fn build_header(
275 file_ref: &str,
276 short: &str,
277 ext: &str,
278 content: &str,
279 line_count: usize,
280 include_deps: bool,
281) -> String {
282 let mut header = format!("{file_ref}={short} {line_count}L");
283
284 if include_deps {
285 let dep_info = deps::extract_deps(content, ext);
286 if !dep_info.imports.is_empty() {
287 let imports_str: Vec<&str> = dep_info
288 .imports
289 .iter()
290 .take(8)
291 .map(|s| s.as_str())
292 .collect();
293 header.push_str(&format!("\n deps {}", imports_str.join(",")));
294 }
295 if !dep_info.exports.is_empty() {
296 let exports_str: Vec<&str> = dep_info
297 .exports
298 .iter()
299 .take(8)
300 .map(|s| s.as_str())
301 .collect();
302 header.push_str(&format!("\n exports {}", exports_str.join(",")));
303 }
304 }
305
306 header
307}
308
309#[allow(clippy::too_many_arguments)]
310fn process_mode(
311 content: &str,
312 mode: &str,
313 file_ref: &str,
314 short: &str,
315 ext: &str,
316 original_tokens: usize,
317 crp_mode: CrpMode,
318 file_path: &str,
319 task: Option<&str>,
320) -> String {
321 let line_count = content.lines().count();
322
323 match mode {
324 "auto" => {
325 let sig =
326 crate::core::mode_predictor::FileSignature::from_path(file_path, original_tokens);
327 let predictor = crate::core::mode_predictor::ModePredictor::new();
328 let resolved = predictor
329 .predict_best_mode(&sig)
330 .unwrap_or_else(|| "full".to_string());
331 process_mode(
332 content,
333 &resolved,
334 file_ref,
335 short,
336 ext,
337 original_tokens,
338 crp_mode,
339 file_path,
340 task,
341 )
342 }
343 "signatures" => {
344 let sigs = signatures::extract_signatures(content, ext);
345 let dep_info = deps::extract_deps(content, ext);
346
347 let mut output = format!("{file_ref}={short} {line_count}L");
348 if !dep_info.imports.is_empty() {
349 let imports_str: Vec<&str> = dep_info
350 .imports
351 .iter()
352 .take(8)
353 .map(|s| s.as_str())
354 .collect();
355 output.push_str(&format!("\n deps {}", imports_str.join(",")));
356 }
357 for sig in &sigs {
358 output.push('\n');
359 if crp_mode.is_tdd() {
360 output.push_str(&sig.to_tdd());
361 } else {
362 output.push_str(&sig.to_compact());
363 }
364 }
365 let sent = count_tokens(&output);
366 let savings = protocol::format_savings(original_tokens, sent);
367 format!("{output}\n{savings}")
368 }
369 "map" => {
370 if ext == "php" {
371 if let Some(php_map) = crate::core::patterns::php::compress_php_map(content, short)
372 {
373 let mut output = format!("{file_ref}={short} {line_count}L\n{php_map}");
374 let sent = count_tokens(&output);
375 let savings = protocol::format_savings(original_tokens, sent);
376 output.push('\n');
377 output.push_str(&savings);
378 return output;
379 }
380 }
381
382 let sigs = signatures::extract_signatures(content, ext);
383 let dep_info = deps::extract_deps(content, ext);
384
385 let mut output = format!("{file_ref}={short} {line_count}L");
386
387 if !dep_info.imports.is_empty() {
388 output.push_str("\n deps: ");
389 output.push_str(&dep_info.imports.join(", "));
390 }
391
392 if !dep_info.exports.is_empty() {
393 output.push_str("\n exports: ");
394 output.push_str(&dep_info.exports.join(", "));
395 }
396
397 let key_sigs: Vec<&signatures::Signature> = sigs
398 .iter()
399 .filter(|s| s.is_exported || s.indent == 0)
400 .collect();
401
402 if !key_sigs.is_empty() {
403 output.push_str("\n API:");
404 for sig in &key_sigs {
405 output.push_str("\n ");
406 if crp_mode.is_tdd() {
407 output.push_str(&sig.to_tdd());
408 } else {
409 output.push_str(&sig.to_compact());
410 }
411 }
412 }
413
414 let sent = count_tokens(&output);
415 let savings = protocol::format_savings(original_tokens, sent);
416 format!("{output}\n{savings}")
417 }
418 "aggressive" => {
419 let raw = compressor::aggressive_compress(content, Some(ext));
420 let compressed = compressor::safeguard_ratio(content, &raw);
421 let header = build_header(file_ref, short, ext, content, line_count, true);
422
423 let mut sym = SymbolMap::new();
424 let idents = symbol_map::extract_identifiers(&compressed, ext);
425 for ident in &idents {
426 sym.register(ident);
427 }
428
429 let sym_beneficial = if sym.len() >= 3 {
430 let sym_table = sym.format_table();
431 let sym_applied = sym.apply(&compressed);
432 let orig_tok = count_tokens(&compressed);
433 let comp_tok = count_tokens(&sym_applied) + count_tokens(&sym_table);
434 let net = orig_tok.saturating_sub(comp_tok);
435 orig_tok > 0 && net * 100 / orig_tok >= 5
436 } else {
437 false
438 };
439
440 if sym_beneficial {
441 let sym_output = sym.apply(&compressed);
442 let sym_table = sym.format_table();
443 let sent = count_tokens(&sym_output) + count_tokens(&sym_table);
444 let savings = protocol::format_savings(original_tokens, sent);
445 return format!("{header}\n{sym_output}{sym_table}\n{savings}");
446 }
447
448 let sent = count_tokens(&compressed);
449 let savings = protocol::format_savings(original_tokens, sent);
450 format!("{header}\n{compressed}\n{savings}")
451 }
452 "entropy" => {
453 let result = entropy::entropy_compress_adaptive(content, file_path);
454 let avg_h = entropy::analyze_entropy(content).avg_entropy;
455 let header = build_header(file_ref, short, ext, content, line_count, false);
456 let techs = result.techniques.join(", ");
457 let output = format!("{header} H̄={avg_h:.1} [{techs}]\n{}", result.output);
458 let sent = count_tokens(&output);
459 let savings = protocol::format_savings(original_tokens, sent);
460 format!("{output}\n{savings}")
461 }
462 "task" => {
463 let task_str = task.unwrap_or("");
464 if task_str.is_empty() {
465 let header = build_header(file_ref, short, ext, content, line_count, true);
466 return format!("{header}\n{content}\n[task mode: no task set — returned full]");
467 }
468 let (_files, keywords) = crate::core::task_relevance::parse_task_hints(task_str);
469 if keywords.is_empty() {
470 let header = build_header(file_ref, short, ext, content, line_count, true);
471 return format!(
472 "{header}\n{content}\n[task mode: no keywords extracted — returned full]"
473 );
474 }
475 let filtered =
476 crate::core::task_relevance::information_bottleneck_filter(content, &keywords, 0.3);
477 let filtered_lines = filtered.lines().count();
478 let header = format!(
479 "{file_ref}={short} {line_count}L [task-filtered: {line_count}→{filtered_lines}]"
480 );
481 let sent = count_tokens(&filtered) + count_tokens(&header);
482 let savings = protocol::format_savings(original_tokens, sent);
483 format!("{header}\n{filtered}\n{savings}")
484 }
485 "reference" => {
486 let tok = count_tokens(content);
487 let output = format!("{file_ref}={short}: {line_count} lines, {tok} tok ({ext})");
488 let sent = count_tokens(&output);
489 let savings = protocol::format_savings(original_tokens, sent);
490 format!("{output}\n{savings}")
491 }
492 mode if mode.starts_with("lines:") => {
493 let range_str = &mode[6..];
494 let extracted = extract_line_range(content, range_str);
495 let header = format!("{file_ref}={short} {line_count}L lines:{range_str}");
496 let sent = count_tokens(&extracted);
497 let savings = protocol::format_savings(original_tokens, sent);
498 format!("{header}\n{extracted}\n{savings}")
499 }
500 _ => {
501 let header = build_header(file_ref, short, ext, content, line_count, true);
502 format!("{header}\n{content}")
503 }
504 }
505}
506
507fn extract_line_range(content: &str, range_str: &str) -> String {
508 let lines: Vec<&str> = content.lines().collect();
509 let total = lines.len();
510 let mut selected = Vec::new();
511
512 for part in range_str.split(',') {
513 let part = part.trim();
514 if let Some((start_s, end_s)) = part.split_once('-') {
515 let start = start_s.trim().parse::<usize>().unwrap_or(1).max(1);
516 let end = end_s.trim().parse::<usize>().unwrap_or(total).min(total);
517 for i in start..=end {
518 if i >= 1 && i <= total {
519 selected.push(format!("{i:>4}| {}", lines[i - 1]));
520 }
521 }
522 } else if let Ok(n) = part.parse::<usize>() {
523 if n >= 1 && n <= total {
524 selected.push(format!("{n:>4}| {}", lines[n - 1]));
525 }
526 }
527 }
528
529 if selected.is_empty() {
530 "No lines matched the range.".to_string()
531 } else {
532 selected.join("\n")
533 }
534}
535
536fn handle_diff(cache: &mut SessionCache, path: &str, file_ref: &str) -> String {
537 let short = protocol::shorten_path(path);
538 let old_content = cache.get(path).map(|e| e.content.clone());
539
540 let new_content = match read_file_lossy(path) {
541 Ok(c) => c,
542 Err(e) => return format!("ERROR: {e}"),
543 };
544
545 let original_tokens = count_tokens(&new_content);
546
547 let diff_output = if let Some(old) = &old_content {
548 compressor::diff_content(old, &new_content)
549 } else {
550 format!("[first read]\n{new_content}")
551 };
552
553 cache.store(path, new_content);
554
555 let sent = count_tokens(&diff_output);
556 let savings = protocol::format_savings(original_tokens, sent);
557 format!("{file_ref}={short} [diff]\n{diff_output}\n{savings}")
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn test_header_toon_format_no_brackets() {
566 let content = "use std::io;\nfn main() {}\n";
567 let header = build_header("F1", "main.rs", "rs", content, 2, false);
568 assert!(!header.contains('['));
569 assert!(!header.contains(']'));
570 assert!(header.contains("F1=main.rs 2L"));
571 }
572
573 #[test]
574 fn test_header_toon_deps_indented() {
575 let content = "use crate::core::cache;\nuse crate::tools;\npub fn main() {}\n";
576 let header = build_header("F1", "main.rs", "rs", content, 3, true);
577 if header.contains("deps") {
578 assert!(
579 header.contains("\n deps "),
580 "deps should use indented TOON format"
581 );
582 assert!(
583 !header.contains("deps:["),
584 "deps should not use bracket format"
585 );
586 }
587 }
588
589 #[test]
590 fn test_header_toon_saves_tokens() {
591 let content = "use crate::foo;\nuse crate::bar;\npub fn baz() {}\npub fn qux() {}\n";
592 let old_header = "F1=main.rs [4L +] deps:[foo,bar] exports:[baz,qux]".to_string();
593 let new_header = build_header("F1", "main.rs", "rs", content, 4, true);
594 let old_tokens = count_tokens(&old_header);
595 let new_tokens = count_tokens(&new_header);
596 assert!(
597 new_tokens <= old_tokens,
598 "TOON header ({new_tokens} tok) should be <= old format ({old_tokens} tok)"
599 );
600 }
601
602 #[test]
603 fn test_tdd_symbols_are_compact() {
604 let symbols = [
605 "⊕", "⊖", "∆", "→", "⇒", "✓", "✗", "⚠", "λ", "§", "∂", "τ", "ε",
606 ];
607 for sym in &symbols {
608 let tok = count_tokens(sym);
609 assert!(tok <= 2, "Symbol {sym} should be 1-2 tokens, got {tok}");
610 }
611 }
612
613 #[test]
614 fn test_task_mode_filters_content() {
615 let content = (0..200)
616 .map(|i| {
617 if i % 20 == 0 {
618 format!("fn validate_token(token: &str) -> bool {{ /* line {i} */ }}")
619 } else {
620 format!("fn unrelated_helper_{i}(x: i32) -> i32 {{ x + {i} }}")
621 }
622 })
623 .collect::<Vec<_>>()
624 .join("\n");
625 let full_tokens = count_tokens(&content);
626 let task = Some("fix bug in validate_token");
627 let result = process_mode(
628 &content,
629 "task",
630 "F1",
631 "test.rs",
632 "rs",
633 full_tokens,
634 CrpMode::Off,
635 "test.rs",
636 task,
637 );
638 let result_tokens = count_tokens(&result);
639 assert!(
640 result_tokens < full_tokens,
641 "task mode ({result_tokens} tok) should be less than full ({full_tokens} tok)"
642 );
643 assert!(
644 result.contains("task-filtered"),
645 "output should contain task-filtered marker"
646 );
647 }
648
649 #[test]
650 fn test_task_mode_without_task_returns_full() {
651 let content = "fn main() {}\nfn helper() {}\n";
652 let tokens = count_tokens(content);
653 let result = process_mode(
654 content,
655 "task",
656 "F1",
657 "test.rs",
658 "rs",
659 tokens,
660 CrpMode::Off,
661 "test.rs",
662 None,
663 );
664 assert!(
665 result.contains("no task set"),
666 "should indicate no task: {result}"
667 );
668 }
669
670 #[test]
671 fn test_reference_mode_one_line() {
672 let content = "fn main() {}\nfn helper() {}\nfn other() {}\n";
673 let tokens = count_tokens(content);
674 let result = process_mode(
675 content,
676 "reference",
677 "F1",
678 "test.rs",
679 "rs",
680 tokens,
681 CrpMode::Off,
682 "test.rs",
683 None,
684 );
685 let lines: Vec<&str> = result.lines().collect();
686 assert!(
687 lines.len() <= 3,
688 "reference mode should be very compact, got {} lines",
689 lines.len()
690 );
691 assert!(result.contains("lines"), "should contain line count");
692 assert!(result.contains("tok"), "should contain token count");
693 }
694
695 #[test]
696 fn benchmark_task_conditioned_compression() {
697 let content = generate_benchmark_code(500);
698 let full_tokens = count_tokens(&content);
699 let task = Some("fix authentication in validate_token");
700
701 let full_output = process_mode(
702 &content,
703 "full",
704 "F1",
705 "server.rs",
706 "rs",
707 full_tokens,
708 CrpMode::Off,
709 "server.rs",
710 task,
711 );
712 let task_output = process_mode(
713 &content,
714 "task",
715 "F1",
716 "server.rs",
717 "rs",
718 full_tokens,
719 CrpMode::Off,
720 "server.rs",
721 task,
722 );
723 let sig_output = process_mode(
724 &content,
725 "signatures",
726 "F1",
727 "server.rs",
728 "rs",
729 full_tokens,
730 CrpMode::Off,
731 "server.rs",
732 task,
733 );
734 let ref_output = process_mode(
735 &content,
736 "reference",
737 "F1",
738 "server.rs",
739 "rs",
740 full_tokens,
741 CrpMode::Off,
742 "server.rs",
743 task,
744 );
745
746 let full_tok = count_tokens(&full_output);
747 let task_tok = count_tokens(&task_output);
748 let sig_tok = count_tokens(&sig_output);
749 let ref_tok = count_tokens(&ref_output);
750
751 eprintln!("\n=== Task-Conditioned Compression Benchmark ===");
752 eprintln!("Source: 500-line Rust file, task='fix authentication in validate_token'");
753 eprintln!(" full: {full_tok:>6} tokens (baseline)");
754 eprintln!(
755 " task: {task_tok:>6} tokens ({:.0}% savings)",
756 (1.0 - task_tok as f64 / full_tok as f64) * 100.0
757 );
758 eprintln!(
759 " signatures: {sig_tok:>6} tokens ({:.0}% savings)",
760 (1.0 - sig_tok as f64 / full_tok as f64) * 100.0
761 );
762 eprintln!(
763 " reference: {ref_tok:>6} tokens ({:.0}% savings)",
764 (1.0 - ref_tok as f64 / full_tok as f64) * 100.0
765 );
766 eprintln!("================================================\n");
767
768 assert!(task_tok < full_tok, "task mode should save tokens");
769 assert!(sig_tok < full_tok, "signatures should save tokens");
770 assert!(ref_tok < sig_tok, "reference should be most compact");
771 }
772
773 fn generate_benchmark_code(lines: usize) -> String {
774 let mut code = Vec::with_capacity(lines);
775 code.push("use std::collections::HashMap;".to_string());
776 code.push("use crate::core::auth;".to_string());
777 code.push(String::new());
778 code.push("pub struct Server {".to_string());
779 code.push(" config: Config,".to_string());
780 code.push(" cache: HashMap<String, String>,".to_string());
781 code.push("}".to_string());
782 code.push(String::new());
783 code.push("impl Server {".to_string());
784 code.push(
785 " pub fn validate_token(&self, token: &str) -> Result<Claims, AuthError> {"
786 .to_string(),
787 );
788 code.push(" let decoded = auth::decode_jwt(token)?;".to_string());
789 code.push(" if decoded.exp < chrono::Utc::now().timestamp() {".to_string());
790 code.push(" return Err(AuthError::Expired);".to_string());
791 code.push(" }".to_string());
792 code.push(" Ok(decoded.claims)".to_string());
793 code.push(" }".to_string());
794 code.push(String::new());
795
796 let remaining = lines.saturating_sub(code.len());
797 for i in 0..remaining {
798 if i % 30 == 0 {
799 code.push(format!(
800 " pub fn handler_{i}(&self, req: Request) -> Response {{"
801 ));
802 } else if i % 30 == 29 {
803 code.push(" }".to_string());
804 } else {
805 code.push(format!(" let val_{i} = self.cache.get(\"key_{i}\").unwrap_or(&\"default\".to_string());"));
806 }
807 }
808 code.push("}".to_string());
809 code.join("\n")
810 }
811}