1use std::collections::HashMap;
7use std::fs;
8use std::io::{self, BufRead, BufReader, Write};
9use std::path::Path;
10
11pub const VERSION: &str = env!("CARGO_PKG_VERSION");
13
14#[derive(Debug, Clone)]
42pub struct LinoEnv {
43 file_path: String,
44 data: HashMap<String, Vec<String>>,
45}
46
47impl LinoEnv {
48 #[must_use]
61 pub fn new<P: AsRef<str>>(file_path: P) -> Self {
62 Self {
63 file_path: file_path.as_ref().to_string(),
64 data: HashMap::new(),
65 }
66 }
67
68 pub fn read(&mut self) -> io::Result<&mut Self> {
85 self.data.clear();
86
87 let path = Path::new(&self.file_path);
88 if !path.exists() {
89 return Ok(self);
90 }
91
92 let file = fs::File::open(path)?;
93 let reader = BufReader::new(file);
94
95 for line in reader.lines() {
96 let line = line?;
97 let trimmed = line.trim();
98
99 if trimmed.is_empty() || trimmed.starts_with('#') {
101 continue;
102 }
103
104 if let Some(separator_index) = line.find(": ") {
106 let key = line[..separator_index].trim().to_string();
107 let value = line[separator_index + 2..].to_string(); self.data.entry(key).or_default().push(value);
110 }
111 }
112
113 Ok(self)
114 }
115
116 #[must_use]
136 pub fn get(&self, reference: &str) -> Option<String> {
137 self.data
138 .get(reference)
139 .and_then(|values| values.last().cloned())
140 }
141
142 #[must_use]
162 pub fn get_all(&self, reference: &str) -> Vec<String> {
163 self.data.get(reference).cloned().unwrap_or_default()
164 }
165
166 pub fn set(&mut self, reference: &str, value: &str) -> &mut Self {
186 self.data
187 .insert(reference.to_string(), vec![value.to_string()]);
188 self
189 }
190
191 pub fn add(&mut self, reference: &str, value: &str) -> &mut Self {
208 self.data
209 .entry(reference.to_string())
210 .or_default()
211 .push(value.to_string());
212 self
213 }
214
215 pub fn write(&self) -> io::Result<&Self> {
237 let mut file = fs::File::create(&self.file_path)?;
238
239 for (key, values) in &self.data {
240 for value in values {
241 writeln!(file, "{key}: {value}")?;
242 }
243 }
244
245 Ok(self)
246 }
247
248 #[must_use]
268 pub fn has(&self, reference: &str) -> bool {
269 self.data
270 .get(reference)
271 .is_some_and(|values| !values.is_empty())
272 }
273
274 pub fn delete(&mut self, reference: &str) -> &mut Self {
290 self.data.remove(reference);
291 self
292 }
293
294 #[must_use]
312 pub fn keys(&self) -> Vec<String> {
313 self.data.keys().cloned().collect()
314 }
315
316 #[must_use]
334 pub fn to_hash_map(&self) -> HashMap<String, String> {
335 let mut result = HashMap::new();
336 for (key, values) in &self.data {
337 if let Some(last_value) = values.last() {
338 result.insert(key.clone(), last_value.clone());
339 }
340 }
341 result
342 }
343}
344
345pub fn read_lino_env<P: AsRef<str>>(file_path: P) -> io::Result<LinoEnv> {
363 let mut env = LinoEnv::new(file_path);
364 env.read()?;
365 Ok(env)
366}
367
368#[allow(clippy::implicit_hasher)]
396pub fn write_lino_env<P: AsRef<str>>(
397 file_path: P,
398 data: &HashMap<String, String>,
399) -> io::Result<LinoEnv> {
400 let mut env = LinoEnv::new(file_path);
401 for (key, value) in data {
402 env.set(key, value);
403 }
404 env.write()?;
405 Ok(env)
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use std::fs;
412
413 fn cleanup(path: &str) {
414 fs::remove_file(path).ok();
415 }
416
417 fn test_file(name: &str) -> String {
418 std::env::temp_dir()
419 .join(format!("lino_env_test_{name}.lenv"))
420 .to_string_lossy()
421 .to_string()
422 }
423
424 mod basic_tests {
425 use super::*;
426
427 #[test]
428 fn test_create_and_write() {
429 let test_file = test_file("basic_create_write");
430 cleanup(&test_file);
431 let mut env = LinoEnv::new(&test_file);
432 env.set("GITHUB_TOKEN", "gh_test123");
433 env.set("TELEGRAM_TOKEN", "054test456");
434 env.write().unwrap();
435
436 assert!(Path::new(&test_file).exists());
437 cleanup(&test_file);
438 }
439
440 #[test]
441 fn test_read() {
442 let test_file = test_file("basic_read");
443 cleanup(&test_file);
444 let mut env1 = LinoEnv::new(&test_file);
446 env1.set("GITHUB_TOKEN", "gh_test123");
447 env1.set("TELEGRAM_TOKEN", "054test456");
448 env1.write().unwrap();
449
450 let mut env2 = LinoEnv::new(&test_file);
452 env2.read().unwrap();
453
454 assert_eq!(env2.get("GITHUB_TOKEN"), Some("gh_test123".to_string()));
455 assert_eq!(env2.get("TELEGRAM_TOKEN"), Some("054test456".to_string()));
456 cleanup(&test_file);
457 }
458 }
459
460 mod get_tests {
461 use super::*;
462
463 #[test]
464 fn test_get_last_instance() {
465 let test_file = test_file("get_last_instance");
466 cleanup(&test_file);
467 let mut env = LinoEnv::new(&test_file);
468 env.add("API_KEY", "value1");
469 env.add("API_KEY", "value2");
470 env.add("API_KEY", "value3");
471
472 assert_eq!(env.get("API_KEY"), Some("value3".to_string()));
473 cleanup(&test_file);
474 }
475
476 #[test]
477 fn test_get_nonexistent() {
478 let test_file = test_file("get_nonexistent");
479 let env = LinoEnv::new(&test_file);
480 assert_eq!(env.get("NON_EXISTENT"), None);
481 }
482 }
483
484 mod get_all_tests {
485 use super::*;
486
487 #[test]
488 fn test_get_all_instances() {
489 let test_file = test_file("get_all_instances");
490 cleanup(&test_file);
491 let mut env = LinoEnv::new(&test_file);
492 env.add("API_KEY", "value1");
493 env.add("API_KEY", "value2");
494 env.add("API_KEY", "value3");
495
496 assert_eq!(env.get_all("API_KEY"), vec!["value1", "value2", "value3"]);
497 cleanup(&test_file);
498 }
499
500 #[test]
501 fn test_get_all_nonexistent() {
502 let test_file = test_file("get_all_nonexistent");
503 let env = LinoEnv::new(&test_file);
504 assert!(env.get_all("NON_EXISTENT").is_empty());
505 }
506 }
507
508 mod set_tests {
509 use super::*;
510
511 #[test]
512 fn test_set_replaces_all() {
513 let test_file = test_file("set_replaces_all");
514 cleanup(&test_file);
515 let mut env = LinoEnv::new(&test_file);
516 env.add("API_KEY", "value1");
517 env.add("API_KEY", "value2");
518 env.set("API_KEY", "new_value");
519
520 assert_eq!(env.get("API_KEY"), Some("new_value".to_string()));
521 assert_eq!(env.get_all("API_KEY"), vec!["new_value"]);
522 cleanup(&test_file);
523 }
524 }
525
526 mod add_tests {
527 use super::*;
528
529 #[test]
530 fn test_add_duplicates() {
531 let test_file = test_file("add_duplicates");
532 cleanup(&test_file);
533 let mut env = LinoEnv::new(&test_file);
534 env.add("KEY", "value1");
535 env.add("KEY", "value2");
536 env.add("KEY", "value3");
537
538 assert_eq!(env.get_all("KEY"), vec!["value1", "value2", "value3"]);
539 cleanup(&test_file);
540 }
541 }
542
543 mod has_tests {
544 use super::*;
545
546 #[test]
547 fn test_has_existing() {
548 let test_file = test_file("has_existing");
549 cleanup(&test_file);
550 let mut env = LinoEnv::new(&test_file);
551 env.set("KEY", "value");
552
553 assert!(env.has("KEY"));
554 cleanup(&test_file);
555 }
556
557 #[test]
558 fn test_has_nonexistent() {
559 let test_file = test_file("has_nonexistent");
560 let env = LinoEnv::new(&test_file);
561 assert!(!env.has("NON_EXISTENT"));
562 }
563 }
564
565 mod delete_tests {
566 use super::*;
567
568 #[test]
569 fn test_delete_all_instances() {
570 let test_file = test_file("delete_all_instances");
571 cleanup(&test_file);
572 let mut env = LinoEnv::new(&test_file);
573 env.add("KEY", "value1");
574 env.add("KEY", "value2");
575 env.delete("KEY");
576
577 assert!(!env.has("KEY"));
578 assert_eq!(env.get("KEY"), None);
579 cleanup(&test_file);
580 }
581 }
582
583 mod keys_tests {
584 use super::*;
585
586 #[test]
587 fn test_keys() {
588 let test_file = test_file("keys");
589 cleanup(&test_file);
590 let mut env = LinoEnv::new(&test_file);
591 env.set("KEY1", "value1");
592 env.set("KEY2", "value2");
593 env.set("KEY3", "value3");
594
595 let keys = env.keys();
596 assert!(keys.contains(&"KEY1".to_string()));
597 assert!(keys.contains(&"KEY2".to_string()));
598 assert!(keys.contains(&"KEY3".to_string()));
599 assert_eq!(keys.len(), 3);
600 cleanup(&test_file);
601 }
602 }
603
604 mod to_hash_map_tests {
605 use super::*;
606
607 #[test]
608 fn test_to_hash_map() {
609 let test_file = test_file("to_hash_map");
610 cleanup(&test_file);
611 let mut env = LinoEnv::new(&test_file);
612 env.add("KEY1", "value1a");
613 env.add("KEY1", "value1b");
614 env.set("KEY2", "value2");
615
616 let obj = env.to_hash_map();
617 assert_eq!(obj.get("KEY1"), Some(&"value1b".to_string()));
618 assert_eq!(obj.get("KEY2"), Some(&"value2".to_string()));
619 cleanup(&test_file);
620 }
621 }
622
623 mod persistence_tests {
624 use super::*;
625
626 #[test]
627 fn test_persist_duplicates() {
628 let test_file = test_file("persist_duplicates");
629 cleanup(&test_file);
630 let mut env1 = LinoEnv::new(&test_file);
631 env1.add("KEY", "value1");
632 env1.add("KEY", "value2");
633 env1.add("KEY", "value3");
634 env1.write().unwrap();
635
636 let mut env2 = LinoEnv::new(&test_file);
637 env2.read().unwrap();
638
639 assert_eq!(env2.get_all("KEY"), vec!["value1", "value2", "value3"]);
640 assert_eq!(env2.get("KEY"), Some("value3".to_string()));
641 cleanup(&test_file);
642 }
643 }
644
645 mod convenience_function_tests {
646 use super::*;
647
648 #[test]
649 fn test_read_lino_env() {
650 let test_file_path = test_file("convenience_read");
651 cleanup(&test_file_path);
652 let mut data = HashMap::new();
653 data.insert("GITHUB_TOKEN".to_string(), "gh_test".to_string());
654 data.insert("TELEGRAM_TOKEN".to_string(), "054test".to_string());
655 write_lino_env(&test_file_path, &data).unwrap();
656
657 let env = read_lino_env(&test_file_path).unwrap();
658 assert_eq!(env.get("GITHUB_TOKEN"), Some("gh_test".to_string()));
659 assert_eq!(env.get("TELEGRAM_TOKEN"), Some("054test".to_string()));
660 cleanup(&test_file_path);
661 }
662
663 #[test]
664 fn test_write_lino_env() {
665 let test_file_path = test_file("convenience_write");
666 cleanup(&test_file_path);
667 let mut data = HashMap::new();
668 data.insert("API_KEY".to_string(), "test_key".to_string());
669 data.insert("SECRET".to_string(), "test_secret".to_string());
670 write_lino_env(&test_file_path, &data).unwrap();
671
672 let env = read_lino_env(&test_file_path).unwrap();
673 assert_eq!(env.get("API_KEY"), Some("test_key".to_string()));
674 assert_eq!(env.get("SECRET"), Some("test_secret".to_string()));
675 cleanup(&test_file_path);
676 }
677 }
678
679 mod format_tests {
680 use super::*;
681
682 #[test]
683 fn test_values_with_colons() {
684 let test_file_path = test_file("format_colons");
685 cleanup(&test_file_path);
686 let mut env = LinoEnv::new(&test_file_path);
687 env.set("URL", "https://example.com:8080");
688 env.write().unwrap();
689
690 let mut env2 = LinoEnv::new(&test_file_path);
691 env2.read().unwrap();
692 assert_eq!(
693 env2.get("URL"),
694 Some("https://example.com:8080".to_string())
695 );
696 cleanup(&test_file_path);
697 }
698
699 #[test]
700 fn test_values_with_spaces() {
701 let test_file_path = test_file("format_spaces");
702 cleanup(&test_file_path);
703 let mut env = LinoEnv::new(&test_file_path);
704 env.set("MESSAGE", "Hello World");
705 env.write().unwrap();
706
707 let mut env2 = LinoEnv::new(&test_file_path);
708 env2.read().unwrap();
709 assert_eq!(env2.get("MESSAGE"), Some("Hello World".to_string()));
710 cleanup(&test_file_path);
711 }
712 }
713
714 mod edge_case_tests {
715 use super::*;
716
717 #[test]
718 fn test_nonexistent_file() {
719 let test_file_path = test_file("nonexistent");
720 cleanup(&test_file_path);
721 let mut env = LinoEnv::new(&test_file_path);
722 env.read().unwrap();
723
724 assert_eq!(env.get("ANY_KEY"), None);
725 assert!(env.keys().is_empty());
726 }
727
728 #[test]
729 fn test_empty_values() {
730 let test_file_path = test_file("empty_values");
731 cleanup(&test_file_path);
732 let mut env = LinoEnv::new(&test_file_path);
733 env.set("EMPTY_KEY", "");
734 env.write().unwrap();
735
736 let mut env2 = LinoEnv::new(&test_file_path);
737 env2.read().unwrap();
738 assert_eq!(env2.get("EMPTY_KEY"), Some(String::new()));
739 cleanup(&test_file_path);
740 }
741 }
742}