Skip to main content

dkit_core/format/
env.rs

1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4
5use crate::format::{FormatReader, FormatWriter};
6use crate::value::Value;
7
8/// .env 포맷 Reader
9///
10/// `KEY=VALUE` 형식의 환경 변수 파일을 파싱하여 flat한 Object로 변환한다.
11/// - `#` 주석 지원
12/// - 빈 줄 무시
13/// - 큰따옴표/작은따옴표 값 지원
14/// - `export` 접두사 무시
15pub struct EnvReader;
16
17impl EnvReader {
18    /// 한 줄을 파싱하여 (key, value) 쌍을 반환한다.
19    /// 주석이나 빈 줄은 None을 반환한다.
20    fn parse_line(line: &str) -> Option<(String, Value)> {
21        let trimmed = line.trim();
22
23        // 빈 줄이나 주석은 무시
24        if trimmed.is_empty() || trimmed.starts_with('#') {
25            return None;
26        }
27
28        // export 접두사 제거
29        let trimmed = trimmed
30            .strip_prefix("export ")
31            .or_else(|| trimmed.strip_prefix("export\t"))
32            .unwrap_or(trimmed);
33
34        // KEY=VALUE 분리 (첫 번째 '='를 기준으로)
35        let eq_pos = trimmed.find('=')?;
36        let key = trimmed[..eq_pos].trim().to_string();
37        if key.is_empty() {
38            return None;
39        }
40
41        let raw_value = trimmed[eq_pos + 1..].trim();
42        let value = Self::parse_value(raw_value);
43
44        Some((key, Value::String(value)))
45    }
46
47    /// 값 문자열을 파싱한다.
48    /// 큰따옴표/작은따옴표로 감싸진 경우 따옴표를 벗긴다.
49    fn parse_value(raw: &str) -> String {
50        if raw.len() >= 2
51            && ((raw.starts_with('"') && raw.ends_with('"'))
52                || (raw.starts_with('\'') && raw.ends_with('\'')))
53        {
54            let inner = &raw[1..raw.len() - 1];
55            // 큰따옴표 내부의 이스케이프 시퀀스 처리
56            if raw.starts_with('"') {
57                return inner
58                    .replace("\\n", "\n")
59                    .replace("\\r", "\r")
60                    .replace("\\t", "\t")
61                    .replace("\\\"", "\"")
62                    .replace("\\\\", "\\");
63            }
64            // 작은따옴표는 이스케이프 처리 없이 그대로 반환
65            return inner.to_string();
66        }
67
68        // 따옴표 없는 값: 인라인 주석 제거
69        if let Some(comment_pos) = raw.find(" #") {
70            raw[..comment_pos].trim_end().to_string()
71        } else {
72            raw.to_string()
73        }
74    }
75}
76
77impl FormatReader for EnvReader {
78    fn read(&self, input: &str) -> anyhow::Result<Value> {
79        let mut map = IndexMap::new();
80
81        for (line_num, line) in input.lines().enumerate() {
82            match Self::parse_line(line) {
83                Some((key, value)) => {
84                    map.insert(key, value);
85                }
86                None => {
87                    // 주석/빈 줄이 아닌데 '='가 없는 줄은 무시하되,
88                    // 내용이 있는 줄인데 파싱 불가능한 경우 에러
89                    let trimmed = line.trim();
90                    if !trimmed.is_empty()
91                        && !trimmed.starts_with('#')
92                        && !trimmed.starts_with("export ")
93                        && !trimmed.contains('=')
94                    {
95                        return Err(crate::error::DkitError::ParseErrorAt {
96                            format: "ENV".to_string(),
97                            source: "invalid line: expected KEY=VALUE format".to_string().into(),
98                            line: line_num + 1,
99                            column: 1,
100                            line_text: line.to_string(),
101                        }
102                        .into());
103                    }
104                }
105            }
106        }
107
108        Ok(Value::Object(map))
109    }
110
111    fn read_from_reader(&self, mut reader: impl Read) -> anyhow::Result<Value> {
112        let mut input = String::new();
113        reader
114            .read_to_string(&mut input)
115            .map_err(|e| crate::error::DkitError::ParseError {
116                format: "ENV".to_string(),
117                source: Box::new(e),
118            })?;
119        self.read(&input)
120    }
121}
122
123/// .env 포맷 Writer
124///
125/// flat한 Object를 `KEY=VALUE` 형식으로 출력한다.
126/// 중첩 구조는 지원하지 않으며, 값은 문자열로 변환된다.
127pub struct EnvWriter;
128
129impl EnvWriter {
130    /// 값을 .env 포맷 문자열로 변환한다.
131    /// 특수 문자가 포함된 경우 큰따옴표로 감싼다.
132    fn format_value(value: &Value) -> String {
133        match value {
134            Value::String(s) => Self::quote_if_needed(s),
135            Value::Null => String::new(),
136            Value::Bool(b) => b.to_string(),
137            Value::Integer(n) => n.to_string(),
138            Value::Float(f) => f.to_string(),
139            Value::Array(_) | Value::Object(_) => {
140                // 중첩 구조는 JSON 문자열로 직렬화
141                let json = serde_json::to_string(value).unwrap_or_default();
142                format!("'{json}'")
143            }
144        }
145    }
146
147    /// 특수 문자가 포함된 경우 큰따옴표로 감싸고, 이스케이프 처리한다.
148    fn quote_if_needed(s: &str) -> String {
149        if s.is_empty() {
150            return "\"\"".to_string();
151        }
152
153        let needs_quoting = s.contains(' ')
154            || s.contains('#')
155            || s.contains('"')
156            || s.contains('\'')
157            || s.contains('\n')
158            || s.contains('\r')
159            || s.contains('\t')
160            || s.contains('\\');
161
162        if needs_quoting {
163            let escaped = s
164                .replace('\\', "\\\\")
165                .replace('"', "\\\"")
166                .replace('\n', "\\n")
167                .replace('\r', "\\r")
168                .replace('\t', "\\t");
169            format!("\"{escaped}\"")
170        } else {
171            s.to_string()
172        }
173    }
174}
175
176impl FormatWriter for EnvWriter {
177    fn write(&self, value: &Value) -> anyhow::Result<String> {
178        match value {
179            Value::Object(map) => {
180                let mut lines: Vec<String> = Vec::with_capacity(map.len());
181                for (key, val) in map {
182                    let formatted = Self::format_value(val);
183                    lines.push(format!("{key}={formatted}"));
184                }
185                let mut output = lines.join("\n");
186                if !output.is_empty() {
187                    output.push('\n');
188                }
189                Ok(output)
190            }
191            Value::Array(arr) => {
192                // Array of Objects → 첫 번째 요소만 사용하거나 모든 키를 병합
193                if arr.is_empty() {
194                    return Ok(String::new());
195                }
196                // 각 요소가 Object인 경우 병합
197                let mut map = IndexMap::new();
198                for item in arr {
199                    if let Value::Object(obj) = item {
200                        for (k, v) in obj {
201                            map.insert(k.clone(), v.clone());
202                        }
203                    } else {
204                        anyhow::bail!(
205                            "ENV format only supports flat Object data. \
206                             Got array with non-object elements."
207                        );
208                    }
209                }
210                self.write(&Value::Object(map))
211            }
212            _ => {
213                anyhow::bail!(
214                    "ENV format only supports Object (key-value pairs). \
215                     Got: {}",
216                    match value {
217                        Value::Null => "null",
218                        Value::Bool(_) => "boolean",
219                        Value::Integer(_) => "integer",
220                        Value::Float(_) => "float",
221                        Value::String(_) => "string",
222                        _ => "unknown",
223                    }
224                );
225            }
226        }
227    }
228
229    fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
230        let output = self.write(value)?;
231        writer
232            .write_all(output.as_bytes())
233            .map_err(|e| crate::error::DkitError::WriteError {
234                format: "ENV".to_string(),
235                source: Box::new(e),
236            })?;
237        Ok(())
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    // --- EnvReader 테스트 ---
246
247    #[test]
248    fn test_reader_simple() {
249        let reader = EnvReader;
250        let input = "DB_HOST=localhost\nDB_PORT=5432\n";
251        let v = reader.read(input).unwrap();
252        let obj = v.as_object().unwrap();
253        assert_eq!(
254            obj.get("DB_HOST"),
255            Some(&Value::String("localhost".to_string()))
256        );
257        assert_eq!(obj.get("DB_PORT"), Some(&Value::String("5432".to_string())));
258    }
259
260    #[test]
261    fn test_reader_comments() {
262        let reader = EnvReader;
263        let input = "# This is a comment\nKEY=value\n# Another comment\n";
264        let v = reader.read(input).unwrap();
265        let obj = v.as_object().unwrap();
266        assert_eq!(obj.len(), 1);
267        assert_eq!(obj.get("KEY"), Some(&Value::String("value".to_string())));
268    }
269
270    #[test]
271    fn test_reader_empty_lines() {
272        let reader = EnvReader;
273        let input = "A=1\n\n\nB=2\n";
274        let v = reader.read(input).unwrap();
275        let obj = v.as_object().unwrap();
276        assert_eq!(obj.len(), 2);
277    }
278
279    #[test]
280    fn test_reader_double_quoted() {
281        let reader = EnvReader;
282        let input = "MSG=\"hello world\"\n";
283        let v = reader.read(input).unwrap();
284        assert_eq!(
285            v.as_object().unwrap().get("MSG"),
286            Some(&Value::String("hello world".to_string()))
287        );
288    }
289
290    #[test]
291    fn test_reader_single_quoted() {
292        let reader = EnvReader;
293        let input = "MSG='hello world'\n";
294        let v = reader.read(input).unwrap();
295        assert_eq!(
296            v.as_object().unwrap().get("MSG"),
297            Some(&Value::String("hello world".to_string()))
298        );
299    }
300
301    #[test]
302    fn test_reader_export_prefix() {
303        let reader = EnvReader;
304        let input = "export DB_HOST=localhost\nexport DB_PORT=5432\n";
305        let v = reader.read(input).unwrap();
306        let obj = v.as_object().unwrap();
307        assert_eq!(
308            obj.get("DB_HOST"),
309            Some(&Value::String("localhost".to_string()))
310        );
311        assert_eq!(obj.get("DB_PORT"), Some(&Value::String("5432".to_string())));
312    }
313
314    #[test]
315    fn test_reader_escape_sequences() {
316        let reader = EnvReader;
317        let input = "MSG=\"line1\\nline2\"\n";
318        let v = reader.read(input).unwrap();
319        assert_eq!(
320            v.as_object().unwrap().get("MSG"),
321            Some(&Value::String("line1\nline2".to_string()))
322        );
323    }
324
325    #[test]
326    fn test_reader_single_quote_no_escape() {
327        let reader = EnvReader;
328        let input = "MSG='line1\\nline2'\n";
329        let v = reader.read(input).unwrap();
330        assert_eq!(
331            v.as_object().unwrap().get("MSG"),
332            Some(&Value::String("line1\\nline2".to_string()))
333        );
334    }
335
336    #[test]
337    fn test_reader_empty_value() {
338        let reader = EnvReader;
339        let input = "EMPTY=\n";
340        let v = reader.read(input).unwrap();
341        assert_eq!(
342            v.as_object().unwrap().get("EMPTY"),
343            Some(&Value::String(String::new()))
344        );
345    }
346
347    #[test]
348    fn test_reader_value_with_equals() {
349        let reader = EnvReader;
350        let input = "URL=https://example.com?key=value\n";
351        let v = reader.read(input).unwrap();
352        assert_eq!(
353            v.as_object().unwrap().get("URL"),
354            Some(&Value::String("https://example.com?key=value".to_string()))
355        );
356    }
357
358    #[test]
359    fn test_reader_inline_comment() {
360        let reader = EnvReader;
361        let input = "KEY=value # this is a comment\n";
362        let v = reader.read(input).unwrap();
363        assert_eq!(
364            v.as_object().unwrap().get("KEY"),
365            Some(&Value::String("value".to_string()))
366        );
367    }
368
369    #[test]
370    fn test_reader_quoted_value_preserves_hash() {
371        let reader = EnvReader;
372        let input = "KEY=\"value # not a comment\"\n";
373        let v = reader.read(input).unwrap();
374        assert_eq!(
375            v.as_object().unwrap().get("KEY"),
376            Some(&Value::String("value # not a comment".to_string()))
377        );
378    }
379
380    #[test]
381    fn test_reader_empty_input() {
382        let reader = EnvReader;
383        let v = reader.read("").unwrap();
384        assert!(v.as_object().unwrap().is_empty());
385    }
386
387    #[test]
388    fn test_reader_whitespace_around_key() {
389        let reader = EnvReader;
390        let input = "  KEY  = value\n";
391        let v = reader.read(input).unwrap();
392        assert_eq!(
393            v.as_object().unwrap().get("KEY"),
394            Some(&Value::String("value".to_string()))
395        );
396    }
397
398    #[test]
399    fn test_reader_from_reader() {
400        let reader = EnvReader;
401        let input = b"KEY=value" as &[u8];
402        let v = reader.read_from_reader(input).unwrap();
403        assert_eq!(
404            v.as_object().unwrap().get("KEY"),
405            Some(&Value::String("value".to_string()))
406        );
407    }
408
409    #[test]
410    fn test_reader_duplicate_keys_last_wins() {
411        let reader = EnvReader;
412        let input = "KEY=first\nKEY=second\n";
413        let v = reader.read(input).unwrap();
414        assert_eq!(
415            v.as_object().unwrap().get("KEY"),
416            Some(&Value::String("second".to_string()))
417        );
418    }
419
420    // --- EnvWriter 테스트 ---
421
422    #[test]
423    fn test_writer_simple() {
424        let writer = EnvWriter;
425        let v = Value::Object({
426            let mut m = IndexMap::new();
427            m.insert("KEY".to_string(), Value::String("value".to_string()));
428            m
429        });
430        let output = writer.write(&v).unwrap();
431        assert_eq!(output, "KEY=value\n");
432    }
433
434    #[test]
435    fn test_writer_multiple_keys() {
436        let writer = EnvWriter;
437        let v = Value::Object({
438            let mut m = IndexMap::new();
439            m.insert("A".to_string(), Value::String("1".to_string()));
440            m.insert("B".to_string(), Value::String("2".to_string()));
441            m
442        });
443        let output = writer.write(&v).unwrap();
444        assert!(output.contains("A=1\n"));
445        assert!(output.contains("B=2\n"));
446    }
447
448    #[test]
449    fn test_writer_quotes_spaces() {
450        let writer = EnvWriter;
451        let v = Value::Object({
452            let mut m = IndexMap::new();
453            m.insert("MSG".to_string(), Value::String("hello world".to_string()));
454            m
455        });
456        let output = writer.write(&v).unwrap();
457        assert_eq!(output, "MSG=\"hello world\"\n");
458    }
459
460    #[test]
461    fn test_writer_empty_value() {
462        let writer = EnvWriter;
463        let v = Value::Object({
464            let mut m = IndexMap::new();
465            m.insert("EMPTY".to_string(), Value::String(String::new()));
466            m
467        });
468        let output = writer.write(&v).unwrap();
469        assert_eq!(output, "EMPTY=\"\"\n");
470    }
471
472    #[test]
473    fn test_writer_null_value() {
474        let writer = EnvWriter;
475        let v = Value::Object({
476            let mut m = IndexMap::new();
477            m.insert("VAL".to_string(), Value::Null);
478            m
479        });
480        let output = writer.write(&v).unwrap();
481        assert_eq!(output, "VAL=\n");
482    }
483
484    #[test]
485    fn test_writer_boolean() {
486        let writer = EnvWriter;
487        let v = Value::Object({
488            let mut m = IndexMap::new();
489            m.insert("DEBUG".to_string(), Value::Bool(true));
490            m
491        });
492        let output = writer.write(&v).unwrap();
493        assert_eq!(output, "DEBUG=true\n");
494    }
495
496    #[test]
497    fn test_writer_integer() {
498        let writer = EnvWriter;
499        let v = Value::Object({
500            let mut m = IndexMap::new();
501            m.insert("PORT".to_string(), Value::Integer(5432));
502            m
503        });
504        let output = writer.write(&v).unwrap();
505        assert_eq!(output, "PORT=5432\n");
506    }
507
508    #[test]
509    fn test_writer_escapes_newline() {
510        let writer = EnvWriter;
511        let v = Value::Object({
512            let mut m = IndexMap::new();
513            m.insert("MSG".to_string(), Value::String("line1\nline2".to_string()));
514            m
515        });
516        let output = writer.write(&v).unwrap();
517        assert_eq!(output, "MSG=\"line1\\nline2\"\n");
518    }
519
520    #[test]
521    fn test_writer_non_object_error() {
522        let writer = EnvWriter;
523        let result = writer.write(&Value::String("hello".to_string()));
524        assert!(result.is_err());
525    }
526
527    #[test]
528    fn test_writer_to_writer() {
529        let writer = EnvWriter;
530        let v = Value::Object({
531            let mut m = IndexMap::new();
532            m.insert("KEY".to_string(), Value::String("value".to_string()));
533            m
534        });
535        let mut buf = Vec::new();
536        writer.write_to_writer(&v, &mut buf).unwrap();
537        let output = String::from_utf8(buf).unwrap();
538        assert_eq!(output, "KEY=value\n");
539    }
540
541    // --- 왕복 변환 테스트 ---
542
543    #[test]
544    fn test_roundtrip() {
545        let input = "DB_HOST=localhost\nDB_PORT=5432\nDEBUG=true\n";
546        let reader = EnvReader;
547        let writer = EnvWriter;
548
549        let value = reader.read(input).unwrap();
550        let output = writer.write(&value).unwrap();
551        let value2 = reader.read(&output).unwrap();
552
553        assert_eq!(value, value2);
554    }
555
556    #[test]
557    fn test_roundtrip_quoted() {
558        let input = "MSG=\"hello world\"\nPATH=\"/usr/bin:/usr/local/bin\"\n";
559        let reader = EnvReader;
560        let writer = EnvWriter;
561
562        let value = reader.read(input).unwrap();
563        let output = writer.write(&value).unwrap();
564        let value2 = reader.read(&output).unwrap();
565
566        assert_eq!(value, value2);
567    }
568
569    #[test]
570    fn test_reader_escaped_double_quote() {
571        let reader = EnvReader;
572        let input = r#"MSG="say \"hello\"""#;
573        let v = reader.read(input).unwrap();
574        assert_eq!(
575            v.as_object().unwrap().get("MSG"),
576            Some(&Value::String("say \"hello\"".to_string()))
577        );
578    }
579
580    #[test]
581    fn test_writer_escapes_double_quote() {
582        let writer = EnvWriter;
583        let v = Value::Object({
584            let mut m = IndexMap::new();
585            m.insert(
586                "MSG".to_string(),
587                Value::String("say \"hello\"".to_string()),
588            );
589            m
590        });
591        let output = writer.write(&v).unwrap();
592        assert_eq!(output, "MSG=\"say \\\"hello\\\"\"\n");
593    }
594}