1use serde::Deserialize;
2use tower_lsp_server::ls_types::DiagnosticSeverity;
3
4#[derive(Debug, Deserialize, Default)]
26pub struct DepsConfig {
27 #[serde(default)]
28 pub inlay_hints: InlayHintsConfig,
29 #[serde(default)]
30 pub diagnostics: DiagnosticsConfig,
31 #[serde(default)]
32 pub cache: CacheConfig,
33 #[serde(default)]
34 pub cold_start: ColdStartConfig,
35 #[serde(default)]
36 pub loading_indicator: LoadingIndicatorConfig,
37}
38
39#[derive(Debug, Clone, Deserialize)]
64pub struct InlayHintsConfig {
65 #[serde(default = "default_true")]
66 pub enabled: bool,
67 #[serde(default = "default_up_to_date")]
68 pub up_to_date_text: String,
69 #[serde(default = "default_needs_update")]
70 pub needs_update_text: String,
71}
72
73impl Default for InlayHintsConfig {
74 fn default() -> Self {
75 Self {
76 enabled: true,
77 up_to_date_text: default_up_to_date(),
78 needs_update_text: default_needs_update(),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Deserialize)]
109pub struct DiagnosticsConfig {
110 #[serde(default = "default_outdated_severity")]
111 pub outdated_severity: DiagnosticSeverity,
112 #[serde(default = "default_unknown_severity")]
113 pub unknown_severity: DiagnosticSeverity,
114 #[serde(default = "default_yanked_severity")]
115 pub yanked_severity: DiagnosticSeverity,
116}
117
118impl Default for DiagnosticsConfig {
119 fn default() -> Self {
120 Self {
121 outdated_severity: default_outdated_severity(),
122 unknown_severity: default_unknown_severity(),
123 yanked_severity: default_yanked_severity(),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Deserialize)]
155pub struct CacheConfig {
156 #[serde(default = "default_refresh_interval")]
157 pub refresh_interval_secs: u64,
158 #[serde(default = "default_true")]
159 pub enabled: bool,
160 #[serde(
162 default = "default_fetch_timeout_secs",
163 deserialize_with = "deserialize_fetch_timeout"
164 )]
165 pub fetch_timeout_secs: u64,
166 #[serde(
168 default = "default_max_concurrent_fetches",
169 deserialize_with = "deserialize_max_concurrent"
170 )]
171 pub max_concurrent_fetches: usize,
172}
173
174impl Default for CacheConfig {
175 fn default() -> Self {
176 Self {
177 refresh_interval_secs: default_refresh_interval(),
178 enabled: true,
179 fetch_timeout_secs: default_fetch_timeout_secs(),
180 max_concurrent_fetches: default_max_concurrent_fetches(),
181 }
182 }
183}
184
185#[derive(Debug, Clone, Deserialize)]
195pub struct LoadingIndicatorConfig {
196 #[serde(default = "default_true")]
198 pub enabled: bool,
199
200 #[serde(default = "default_true")]
202 pub fallback_to_hints: bool,
203
204 #[serde(
207 default = "default_loading_text",
208 deserialize_with = "deserialize_loading_text"
209 )]
210 pub loading_text: String,
211}
212
213impl Default for LoadingIndicatorConfig {
214 fn default() -> Self {
215 Self {
216 enabled: true,
217 fallback_to_hints: true,
218 loading_text: default_loading_text(),
219 }
220 }
221}
222
223const fn default_true() -> bool {
225 true
226}
227
228fn default_up_to_date() -> String {
229 "✅".to_string()
230}
231
232fn default_needs_update() -> String {
233 "❌ {}".to_string()
234}
235
236fn default_loading_text() -> String {
237 "⏳".to_string()
238}
239
240const MAX_LOADING_TEXT_LENGTH: usize = 100;
242
243fn validate_loading_text(text: String) -> String {
245 if text.len() > MAX_LOADING_TEXT_LENGTH {
246 tracing::warn!(
247 "loading_text exceeded max length of {} chars, truncating from {} to {}",
248 MAX_LOADING_TEXT_LENGTH,
249 text.len(),
250 MAX_LOADING_TEXT_LENGTH
251 );
252 text.chars().take(MAX_LOADING_TEXT_LENGTH).collect()
253 } else {
254 text
255 }
256}
257
258fn deserialize_loading_text<'de, D>(deserializer: D) -> Result<String, D::Error>
260where
261 D: serde::Deserializer<'de>,
262{
263 let text = String::deserialize(deserializer)?;
264 Ok(validate_loading_text(text))
265}
266
267const fn default_outdated_severity() -> DiagnosticSeverity {
268 DiagnosticSeverity::HINT
269}
270
271const fn default_unknown_severity() -> DiagnosticSeverity {
272 DiagnosticSeverity::WARNING
273}
274
275const fn default_yanked_severity() -> DiagnosticSeverity {
276 DiagnosticSeverity::WARNING
277}
278
279const fn default_refresh_interval() -> u64 {
280 300 }
282
283const fn default_fetch_timeout_secs() -> u64 {
284 10
285}
286
287const fn default_max_concurrent_fetches() -> usize {
288 5
289}
290
291const MIN_FETCH_TIMEOUT_SECS: u64 = 1;
293const MAX_FETCH_TIMEOUT_SECS: u64 = 300;
295
296const MIN_CONCURRENT_FETCHES: usize = 1;
298const MAX_CONCURRENT_FETCHES: usize = 100;
300
301fn deserialize_fetch_timeout<'de, D>(deserializer: D) -> Result<u64, D::Error>
303where
304 D: serde::Deserializer<'de>,
305{
306 let secs = u64::deserialize(deserializer)?;
307 let clamped = secs.clamp(MIN_FETCH_TIMEOUT_SECS, MAX_FETCH_TIMEOUT_SECS);
308 if clamped != secs {
309 tracing::warn!(
310 "fetch_timeout_secs {} clamped to {} (valid range: {}-{})",
311 secs,
312 clamped,
313 MIN_FETCH_TIMEOUT_SECS,
314 MAX_FETCH_TIMEOUT_SECS
315 );
316 }
317 Ok(clamped)
318}
319
320fn deserialize_max_concurrent<'de, D>(deserializer: D) -> Result<usize, D::Error>
322where
323 D: serde::Deserializer<'de>,
324{
325 let count = usize::deserialize(deserializer)?;
326 let clamped = count.clamp(MIN_CONCURRENT_FETCHES, MAX_CONCURRENT_FETCHES);
327 if clamped != count {
328 tracing::warn!(
329 "max_concurrent_fetches {} clamped to {} (valid range: {}-{})",
330 count,
331 clamped,
332 MIN_CONCURRENT_FETCHES,
333 MAX_CONCURRENT_FETCHES
334 );
335 }
336 Ok(clamped)
337}
338
339#[derive(Debug, Clone, Deserialize)]
367pub struct ColdStartConfig {
368 #[serde(default = "default_true")]
369 pub enabled: bool,
370 #[serde(default = "default_rate_limit_ms")]
371 pub rate_limit_ms: u64,
372}
373
374impl Default for ColdStartConfig {
375 fn default() -> Self {
376 Self {
377 enabled: true,
378 rate_limit_ms: default_rate_limit_ms(),
379 }
380 }
381}
382
383const fn default_rate_limit_ms() -> u64 {
384 100 }
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_default_config() {
393 let config = DepsConfig::default();
394 assert!(config.inlay_hints.enabled);
395 assert_eq!(config.inlay_hints.up_to_date_text, "✅");
396 assert_eq!(config.inlay_hints.needs_update_text, "❌ {}");
397 }
398
399 #[test]
400 fn test_inlay_hints_config_deserialization() {
401 let json = r#"{
402 "enabled": false,
403 "up_to_date_text": "OK",
404 "needs_update_text": "UPDATE {}"
405 }"#;
406
407 let config: InlayHintsConfig = serde_json::from_str(json).unwrap();
408 assert!(!config.enabled);
409 assert_eq!(config.up_to_date_text, "OK");
410 assert_eq!(config.needs_update_text, "UPDATE {}");
411 }
412
413 #[test]
414 fn test_diagnostics_config_deserialization() {
415 let json = r#"{
416 "outdated_severity": 1,
417 "unknown_severity": 2,
418 "yanked_severity": 2
419 }"#;
420
421 let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
422 assert_eq!(config.outdated_severity, DiagnosticSeverity::ERROR);
423 assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
424 assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
425 }
426
427 #[test]
428 fn test_cache_config_deserialization() {
429 let json = r#"{
430 "refresh_interval_secs": 600,
431 "enabled": false
432 }"#;
433
434 let config: CacheConfig = serde_json::from_str(json).unwrap();
435 assert_eq!(config.refresh_interval_secs, 600);
436 assert!(!config.enabled);
437 }
438
439 #[test]
440 fn test_cache_config_defaults() {
441 let config = CacheConfig::default();
442 assert!(config.enabled);
443 assert_eq!(config.refresh_interval_secs, 300);
444 assert_eq!(config.fetch_timeout_secs, 10);
445 assert_eq!(config.max_concurrent_fetches, 5);
446 }
447
448 #[test]
449 fn test_cache_config_with_timeout_and_concurrency() {
450 let json = r#"{
451 "refresh_interval_secs": 600,
452 "enabled": true,
453 "fetch_timeout_secs": 10,
454 "max_concurrent_fetches": 50
455 }"#;
456
457 let config: CacheConfig = serde_json::from_str(json).unwrap();
458 assert_eq!(config.refresh_interval_secs, 600);
459 assert!(config.enabled);
460 assert_eq!(config.fetch_timeout_secs, 10);
461 assert_eq!(config.max_concurrent_fetches, 50);
462 }
463
464 #[test]
465 fn test_full_config_deserialization() {
466 let json = r#"{
467 "inlay_hints": {
468 "enabled": true,
469 "up_to_date_text": "✅",
470 "needs_update_text": "❌ {}"
471 },
472 "diagnostics": {
473 "outdated_severity": 4,
474 "unknown_severity": 2,
475 "yanked_severity": 2
476 },
477 "cache": {
478 "refresh_interval_secs": 300,
479 "enabled": true
480 }
481 }"#;
482
483 let config: DepsConfig = serde_json::from_str(json).unwrap();
484 assert!(config.inlay_hints.enabled);
485 assert_eq!(
486 config.diagnostics.outdated_severity,
487 DiagnosticSeverity::HINT
488 );
489 assert_eq!(config.cache.refresh_interval_secs, 300);
490 }
491
492 #[test]
493 fn test_partial_config_deserialization() {
494 let json = r#"{
495 "inlay_hints": {
496 "enabled": false
497 }
498 }"#;
499
500 let config: DepsConfig = serde_json::from_str(json).unwrap();
501 assert!(!config.inlay_hints.enabled);
502 assert_eq!(config.inlay_hints.up_to_date_text, "✅");
504 assert_eq!(
505 config.diagnostics.outdated_severity,
506 DiagnosticSeverity::HINT
507 );
508 }
509
510 #[test]
511 fn test_empty_config_deserialization() {
512 let json = r"{}";
513 let config: DepsConfig = serde_json::from_str(json).unwrap();
514 assert!(config.inlay_hints.enabled);
516 assert!(config.cache.enabled);
517 }
518
519 #[test]
520 fn test_cold_start_config_defaults() {
521 let config = ColdStartConfig::default();
522 assert!(config.enabled);
523 assert_eq!(config.rate_limit_ms, 100);
524 }
525
526 #[test]
527 fn test_cold_start_config_deserialization() {
528 let json = r#"{
529 "enabled": false,
530 "rate_limit_ms": 200
531 }"#;
532
533 let config: ColdStartConfig = serde_json::from_str(json).unwrap();
534 assert!(!config.enabled);
535 assert_eq!(config.rate_limit_ms, 200);
536 }
537
538 #[test]
539 fn test_full_config_with_cold_start() {
540 let json = r#"{
541 "cold_start": {
542 "enabled": true,
543 "rate_limit_ms": 150
544 }
545 }"#;
546
547 let config: DepsConfig = serde_json::from_str(json).unwrap();
548 assert!(config.cold_start.enabled);
549 assert_eq!(config.cold_start.rate_limit_ms, 150);
550 }
551
552 #[test]
553 fn test_loading_indicator_config_defaults() {
554 let config = LoadingIndicatorConfig::default();
555 assert!(config.enabled);
556 assert!(config.fallback_to_hints);
557 assert_eq!(config.loading_text, "⏳");
558 }
559
560 #[test]
561 fn test_loading_indicator_config_deserialization() {
562 let json = r#"{
563 "enabled": false,
564 "fallback_to_hints": false,
565 "loading_text": "Loading..."
566 }"#;
567
568 let config: LoadingIndicatorConfig = serde_json::from_str(json).unwrap();
569 assert!(!config.enabled);
570 assert!(!config.fallback_to_hints);
571 assert_eq!(config.loading_text, "Loading...");
572 }
573
574 #[test]
575 fn test_loading_text_truncation() {
576 let long_text = "a".repeat(150);
577 let json = format!(
578 r#"{{
579 "enabled": true,
580 "fallback_to_hints": true,
581 "loading_text": "{}"
582 }}"#,
583 long_text
584 );
585
586 let config: LoadingIndicatorConfig = serde_json::from_str(&json).unwrap();
587 assert_eq!(config.loading_text.len(), 100);
588 assert_eq!(config.loading_text, "a".repeat(100));
589 }
590
591 #[test]
592 fn test_loading_text_exactly_100_chars() {
593 let text = "a".repeat(100);
594 let json = format!(
595 r#"{{
596 "enabled": true,
597 "fallback_to_hints": true,
598 "loading_text": "{}"
599 }}"#,
600 text
601 );
602
603 let config: LoadingIndicatorConfig = serde_json::from_str(&json).unwrap();
604 assert_eq!(config.loading_text.len(), 100);
605 assert_eq!(config.loading_text, text);
606 }
607
608 #[test]
609 fn test_loading_text_under_limit() {
610 let json = r#"{
611 "enabled": true,
612 "fallback_to_hints": true,
613 "loading_text": "⏳ Loading dependencies..."
614 }"#;
615
616 let config: LoadingIndicatorConfig = serde_json::from_str(json).unwrap();
617 assert_eq!(config.loading_text, "⏳ Loading dependencies...");
618 assert!(config.loading_text.len() < 100);
619 }
620
621 #[test]
622 fn test_loading_text_default() {
623 let json = r#"{
624 "enabled": true,
625 "fallback_to_hints": true
626 }"#;
627
628 let config: LoadingIndicatorConfig = serde_json::from_str(json).unwrap();
629 assert_eq!(config.loading_text, "⏳");
630 }
631
632 #[test]
633 fn test_cache_config_fetch_timeout_clamped_min() {
634 let json = r#"{"fetch_timeout_secs": 0}"#;
635 let config: CacheConfig = serde_json::from_str(json).unwrap();
636 assert_eq!(config.fetch_timeout_secs, 1, "Should clamp 0 to MIN");
637 }
638
639 #[test]
640 fn test_cache_config_fetch_timeout_clamped_max() {
641 let json = r#"{"fetch_timeout_secs": 999999}"#;
642 let config: CacheConfig = serde_json::from_str(json).unwrap();
643 assert_eq!(config.fetch_timeout_secs, 300, "Should clamp to MAX");
644 }
645
646 #[test]
647 fn test_cache_config_fetch_timeout_valid_range() {
648 let json = r#"{"fetch_timeout_secs": 10}"#;
649 let config: CacheConfig = serde_json::from_str(json).unwrap();
650 assert_eq!(
651 config.fetch_timeout_secs, 10,
652 "Valid value should not be clamped"
653 );
654 }
655
656 #[test]
657 fn test_cache_config_max_concurrent_clamped_min() {
658 let json = r#"{"max_concurrent_fetches": 0}"#;
659 let config: CacheConfig = serde_json::from_str(json).unwrap();
660 assert_eq!(config.max_concurrent_fetches, 1, "Should clamp 0 to MIN");
661 }
662
663 #[test]
664 fn test_cache_config_max_concurrent_clamped_max() {
665 let json = r#"{"max_concurrent_fetches": 100000}"#;
666 let config: CacheConfig = serde_json::from_str(json).unwrap();
667 assert_eq!(config.max_concurrent_fetches, 100, "Should clamp to MAX");
668 }
669
670 #[test]
671 fn test_cache_config_max_concurrent_valid_range() {
672 let json = r#"{"max_concurrent_fetches": 50}"#;
673 let config: CacheConfig = serde_json::from_str(json).unwrap();
674 assert_eq!(
675 config.max_concurrent_fetches, 50,
676 "Valid value should not be clamped"
677 );
678 }
679}