This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-migrate / src / envfile.rs
3.5 kB 114 lines
1use std::collections::BTreeMap; 2use std::path::{Path, PathBuf}; 3 4#[derive(Debug, thiserror::Error)] 5pub enum EnvFileError { 6 #[error("read env file {path}: {source}")] 7 Io { 8 path: PathBuf, 9 source: std::io::Error, 10 }, 11} 12 13#[derive(Debug, Default)] 14pub struct EnvFile { 15 values: BTreeMap<String, String>, 16} 17 18impl EnvFile { 19 pub fn read(path: &Path) -> Result<Self, EnvFileError> { 20 let body = std::fs::read_to_string(path).map_err(|source| EnvFileError::Io { 21 path: path.to_path_buf(), 22 source, 23 })?; 24 Ok(Self::parse(&body)) 25 } 26 27 pub fn parse(body: &str) -> Self { 28 let values = body 29 .lines() 30 .map(str::trim) 31 .filter(|line| !line.is_empty() && !line.starts_with('#')) 32 .map(|line| line.strip_prefix("export ").unwrap_or(line)) 33 .filter_map(|line| line.split_once('=')) 34 .filter_map(|(key, value)| unquote(value).map(|value| (key.trim().to_string(), value))) 35 .collect(); 36 Self { values } 37 } 38 39 pub fn get(&self, key: &str) -> Option<&str> { 40 self.values.get(key).map(String::as_str) 41 } 42} 43 44fn unquote(raw: &str) -> Option<String> { 45 let trimmed = raw.trim(); 46 ['"', '\''] 47 .into_iter() 48 .find_map(|quote| trimmed.strip_prefix(quote).map(|rest| (quote, rest))) 49 .map_or_else( 50 || Some(strip_comment(trimmed).to_string()), 51 |(quote, rest)| rest.split_once(quote).map(|(inner, _)| inner.to_string()), 52 ) 53} 54 55fn strip_comment(value: &str) -> &str { 56 value 57 .match_indices('#') 58 .find(|(index, _)| { 59 value[..*index] 60 .chars() 61 .next_back() 62 .is_some_and(char::is_whitespace) 63 }) 64 .map_or(value, |(index, _)| &value[..index]) 65 .trim_end() 66} 67 68#[cfg(test)] 69mod tests { 70 use super::*; 71 72 #[test] 73 fn parsing_keeps_assignments_and_drops_every_other_line() { 74 let file = EnvFile::parse( 75 r#"# knot1 76KNOT_SERVER_HOSTNAME=oyster.cafe 77export KNOT_SERVER_OWNER="did:plc:nel" 78 79KNOT_SERVER_PLC_URL='https://plc.oyster.cafe' 80broken line 81KNOT_REPO_SCAN_PATH=/data/repos # tangled default 82KNOT_QUOTED_HOSTNAME="nel.pet" # quoted 83KNOT_SERVER_SECRET='a # b' 84KNOT_DEV_FLAGS=#literal 85KNOT_APPVIEW_URL=https://tangled.test/#frag 86KNOT_UNTERMINATED="oyster.cafe 87KNOT_ALSO_UNTERMINATED='did:plc:nel 88KNOT_LAST=kept 89"#, 90 ); 91 assert_eq!(file.get("KNOT_SERVER_HOSTNAME"), Some("oyster.cafe")); 92 assert_eq!(file.get("KNOT_SERVER_OWNER"), Some("did:plc:nel")); 93 assert_eq!( 94 file.get("KNOT_SERVER_PLC_URL"), 95 Some("https://plc.oyster.cafe") 96 ); 97 assert_eq!(file.get("broken"), None); 98 assert_eq!(file.get("KNOT_REPO_SCAN_PATH"), Some("/data/repos")); 99 assert_eq!(file.get("KNOT_QUOTED_HOSTNAME"), Some("nel.pet")); 100 assert_eq!(file.get("KNOT_SERVER_SECRET"), Some("a # b")); 101 assert_eq!(file.get("KNOT_DEV_FLAGS"), Some("#literal")); 102 assert_eq!( 103 file.get("KNOT_APPVIEW_URL"), 104 Some("https://tangled.test/#frag") 105 ); 106 assert_eq!(file.get("KNOT_UNTERMINATED"), None); 107 assert_eq!(file.get("KNOT_ALSO_UNTERMINATED"), None); 108 assert_eq!( 109 file.get("KNOT_LAST"), 110 Some("kept"), 111 "an unterminated quote drops its own line and nothing after it" 112 ); 113 } 114}