Skip to main content

rupring/
application_properties.rs

1/*!
2## Intro
3- application.properties is a configuration method influenced by spring.
4
5## How to find it
6- The rupring program searches the current execution path to see if there is a file called application.properties.
7- If it does not exist, application.properties is searched based on the directory of the current executable file.
8- If it is still not there, load it with default values ​​and start.
9
10## Environment Variables
11- Environment variables in the execution context are also loaded into application.properties.
12- If application.properties and the environment variable have the same key, the environment variable is ignored.
13
14## Format
15- Similar to spring, it has a Key=Value format separated by newlines.
16
17## Special Options
18| Key | Description | Default |
19| --- | --- | --- |
20| environment | The environment to run in. | dev |
21| server.port | The port to listen on. | 3000 |
22| server.address | The address to listen on. | 0.0.0.0 |
23| server.shutdown | The shutdown mode. (immediate,graceful) | immediate |
24| server.timeout-per-shutdown-phase | The timeout per shutdown phase. (e.g. 30s, 1m, 1h) | 30s |
25| server.compression.enabled | Whether to enable compression. | false |
26| server.compression.mime-types | The mime types to compress. | text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml |
27| server.compression.min-response-size | The minimum response size to compress. (byte) | 2048 |
28| server.compression.algorithm | The compression algorithm to use. (gzip,deflate) | gzip |
29| server.thread.limit | The thread limit to use. | None(max) |
30| server.request-timeout | The request timeout. (300 = 300 millisecond, 3s = 3 second, 2m = 2 minute) | No Timeout |
31| server.request.uri.max-length | The max length of the request URI. | None |
32| server.request.header.max-length | The max length of the request header. | None |
33| server.request.header.max-number-of-headers | The number of headers to allow. | None |
34| server.request.body.max-length | The max length of the request body. | 2MB |
35| server.http1.keep-alive | Whether to keep-alive for HTTP/1. (false=disable, true=enable) | false |
36| server.ssl.key | The SSL key file. (SSL is enabled by feature="tls") | None |
37| server.ssl.cert | The SSL cert file. (SSL is enabled by feature="tls") | None |
38| server.multipart.auto-parsing-enabled | Whether to enable auto parsing for multipart. | true |
39| server.cookie.auto-parsing-enabled | Whether to enable auto parsing for cookie. | true |
40| banner.enabled | Whether to enable the banner. | true |
41| banner.location | The location of the banner file. | None |
42| banner.charset | The charset of the banner file. (UTF-8, UTF-16) | UTF-8 |
43*/
44
45use std::{collections::HashMap, net::SocketAddr, time::Duration};
46
47// "250", "10KB", '10MB', "10GB" 같은 표현식을 실제 바이트 단위 정수값으로 변환
48pub fn parse_byte_size(size: &str) -> Option<usize> {
49    let size = size.trim();
50    let size = size.to_uppercase();
51
52    let size = if size.ends_with("B") {
53        size[..size.len() - 1].to_string()
54    } else {
55        size
56    };
57
58    let size = size.replace(",", "");
59
60    let size = if size.ends_with("K") {
61        size[..size.len() - 1].parse::<f64>().unwrap_or(0.0) * 1024.0
62    } else if size.ends_with("M") {
63        size[..size.len() - 1].parse::<f64>().unwrap_or(0.0) * 1024.0 * 1024.0
64    } else if size.ends_with("G") {
65        size[..size.len() - 1].parse::<f64>().unwrap_or(0.0) * 1024.0 * 1024.0 * 1024.0
66    } else {
67        size.parse::<f64>().unwrap_or(0.0)
68    };
69
70    Some(size as usize)
71}
72
73#[derive(Debug, PartialEq, Clone)]
74pub struct ApplicationProperties {
75    pub server: Server,
76    pub environment: String,
77    pub banner: Banner,
78
79    pub etc: HashMap<String, String>,
80}
81
82impl Default for ApplicationProperties {
83    fn default() -> Self {
84        ApplicationProperties {
85            server: Server::default(),
86            environment: "dev".to_string(),
87            etc: HashMap::new(),
88            banner: Banner::default(),
89        }
90    }
91}
92
93#[derive(Debug, PartialEq, Clone)]
94pub enum CompressionAlgorithm {
95    Gzip,
96    Deflate,
97    Unknown(String),
98}
99
100#[derive(Debug, PartialEq, Clone)]
101pub struct Compression {
102    pub enabled: bool,
103    pub mime_types: Vec<String>,
104    pub min_response_size: usize,
105    pub algorithm: CompressionAlgorithm,
106}
107
108impl ToString for CompressionAlgorithm {
109    fn to_string(&self) -> String {
110        match self {
111            CompressionAlgorithm::Gzip => "gzip".to_string(),
112            CompressionAlgorithm::Deflate => "deflate".to_string(),
113            CompressionAlgorithm::Unknown(s) => s.to_string(),
114        }
115    }
116}
117
118impl From<String> for CompressionAlgorithm {
119    fn from(s: String) -> Self {
120        match s.as_str() {
121            "gzip" => CompressionAlgorithm::Gzip,
122            "deflate" => CompressionAlgorithm::Deflate,
123            _ => CompressionAlgorithm::Unknown(s),
124        }
125    }
126}
127
128impl Default for Compression {
129    fn default() -> Self {
130        Compression {
131            enabled: false,
132            mime_types: [
133                "text/html",
134                "text/xml",
135                "text/plain",
136                "text/css",
137                "text/javascript",
138                "application/javascript",
139                "application/json",
140                "application/xml",
141            ]
142            .iter()
143            .map(|s| s.to_string())
144            .collect(),
145            min_response_size: 2048, // 2KB
146            algorithm: CompressionAlgorithm::Gzip,
147        }
148    }
149}
150
151#[derive(Debug, PartialEq, Clone)]
152pub struct Banner {
153    pub enabled: bool,
154    pub charset: String,
155    pub location: Option<String>,
156}
157
158impl Default for Banner {
159    fn default() -> Self {
160        Banner {
161            enabled: true,
162            charset: "UTF-8".to_string(),
163            location: None,
164        }
165    }
166}
167
168#[derive(Debug, PartialEq, Clone)]
169pub enum ShutdownType {
170    Immediate,
171    Graceful,
172}
173
174impl From<String> for ShutdownType {
175    fn from(s: String) -> Self {
176        match s.as_str() {
177            "immediate" => ShutdownType::Immediate,
178            "graceful" => ShutdownType::Graceful,
179            _ => ShutdownType::Immediate,
180        }
181    }
182}
183
184#[derive(Debug, PartialEq, Clone, Default)]
185pub struct SSL {
186    pub key: String,
187    pub cert: String,
188}
189
190#[derive(Debug, PartialEq, Clone, Default)]
191pub struct Http1 {
192    pub keep_alive: bool,
193}
194
195#[derive(Debug, PartialEq, Clone)]
196pub struct Multipart {
197    pub auto_parsing_enabled: bool,
198}
199
200impl Default for Multipart {
201    fn default() -> Self {
202        Multipart {
203            auto_parsing_enabled: true,
204        }
205    }
206}
207
208#[derive(Debug, PartialEq, Clone)]
209pub struct Cookie {
210    pub auto_parsing_enabled: bool,
211}
212
213impl Default for Cookie {
214    fn default() -> Self {
215        Cookie {
216            auto_parsing_enabled: true,
217        }
218    }
219}
220
221#[derive(Debug, PartialEq, Clone, Default)]
222pub struct RequestURIConfig {
223    pub max_length: Option<usize>,
224}
225
226#[derive(Debug, PartialEq, Clone, Default)]
227pub struct RequestHeaderConfig {
228    pub max_length: Option<usize>,
229    pub max_number_of_headers: Option<usize>,
230}
231
232#[derive(Debug, PartialEq, Clone)]
233pub struct RequestBodyConfig {
234    pub max_length: usize,
235}
236
237impl Default for RequestBodyConfig {
238    fn default() -> Self {
239        RequestBodyConfig {
240            max_length: 2 * 1000 * 1000, // 2MB
241        }
242    }
243}
244
245#[derive(Debug, PartialEq, Clone, Default)]
246pub struct RequestConfig {
247    pub uri: RequestURIConfig,
248    pub header: RequestHeaderConfig,
249    pub body: RequestBodyConfig,
250}
251
252// Reference: https://docs.spring.io/spring-boot/appendix/application-properties/index.html#appendix.application-properties.server
253#[derive(Debug, PartialEq, Clone)]
254pub struct Server {
255    pub address: String,
256    pub port: u16,
257    pub compression: Compression,
258    pub shutdown: ShutdownType,
259    pub timeout_per_shutdown_phase: String,
260    pub thread_limit: Option<usize>,
261    pub request_timeout: Option<Duration>,
262    pub http1: Http1,
263    pub ssl: SSL,
264    pub multipart: Multipart,
265    pub cookie: Cookie,
266    pub request: RequestConfig,
267}
268
269impl Server {
270    pub fn make_address(&self) -> anyhow::Result<SocketAddr> {
271        use std::net::{IpAddr, SocketAddr};
272        use std::str::FromStr;
273
274        let port = self.port;
275        let host = self.address.clone();
276
277        let ip = IpAddr::from_str(host.as_str())?;
278
279        let socket_addr = SocketAddr::new(ip, port);
280
281        Ok(socket_addr)
282    }
283}
284
285impl Default for Server {
286    fn default() -> Self {
287        Server {
288            address: "0.0.0.0".to_string(),
289            port: 3000,
290            compression: Compression::default(),
291            shutdown: ShutdownType::Immediate,
292            timeout_per_shutdown_phase: "30s".to_string(),
293            thread_limit: None,
294            request_timeout: None,
295            http1: Http1::default(),
296            ssl: Default::default(),
297            multipart: Default::default(),
298            cookie: Default::default(),
299            request: Default::default(),
300        }
301    }
302}
303
304impl Server {
305    pub fn is_graceful_shutdown(&self) -> bool {
306        self.shutdown == ShutdownType::Graceful
307    }
308
309    pub fn shutdown_timeout_duration(&self) -> std::time::Duration {
310        let timeout = self
311            .timeout_per_shutdown_phase
312            .trim_end_matches(|c| !char::is_numeric(c));
313        let timeout = timeout.parse::<u64>().unwrap_or(30);
314
315        let duration = match self.timeout_per_shutdown_phase.chars().last() {
316            Some('s') => std::time::Duration::from_secs(timeout),
317            Some('m') => std::time::Duration::from_secs(timeout * 60),
318            Some('h') => std::time::Duration::from_secs(timeout * 60 * 60),
319            _ => std::time::Duration::from_secs(30),
320        };
321
322        duration
323    }
324}
325
326impl ApplicationProperties {
327    pub fn from_properties(text: String) -> ApplicationProperties {
328        let mut server = Server::default();
329        let mut environment = "dev".to_string();
330        let mut etc = HashMap::new();
331        let mut banner = Banner::default();
332
333        let mut key_values = HashMap::new();
334
335        // application.properties 파일에서 추출
336        for line in text.lines() {
337            let mut parts = line.split("=");
338
339            let key = match parts.next() {
340                Some(key) => key.trim().to_owned(),
341                None => continue,
342            };
343            let value = match parts.next() {
344                Some(value) => value.trim().to_owned(),
345                None => continue,
346            };
347
348            // value에 앞뒤로 ""가 있다면 제거
349            let value = if value.starts_with('"') && value.ends_with('"') {
350                value[1..value.len() - 1].to_string()
351            } else {
352                value.to_string()
353            };
354
355            key_values.insert(key, value);
356        }
357
358        // 환경변수에서도 추출
359        let env = std::env::vars().collect::<HashMap<_, _>>();
360        for (key, value) in env {
361            if key_values.contains_key(&key) {
362                continue;
363            }
364
365            key_values.insert(key, value);
366        }
367
368        // 추출한 key-value를 바탕으로 기본 정의된 항목은 바인딩, 그 외는 etc에 저장
369        for (key, value) in key_values {
370            // TODO: 매크로 기반 파싱 구현
371            match key.as_str() {
372                "server.port" => {
373                    if let Ok(value) = value.parse::<u16>() {
374                        server.port = value;
375                    }
376                }
377                "server.address" => {
378                    server.address = value.to_string();
379                }
380                "server.shutdown" => server.shutdown = value.into(),
381                "server.timeout-per-shutdown-phase" => {
382                    server.timeout_per_shutdown_phase = value.to_string();
383                }
384                "server.compression.enabled" => {
385                    if let Ok(value) = value.parse::<bool>() {
386                        server.compression.enabled = value;
387                    }
388                }
389                "server.compression.mime-types" => {
390                    server.compression.mime_types =
391                        value.split(",").map(|s| s.to_string()).collect();
392                }
393                "server.compression.min-response-size" => {
394                    if let Ok(value) = value.parse::<usize>() {
395                        server.compression.min_response_size = value;
396                    }
397                }
398                "server.compression.algorithm" => {
399                    server.compression.algorithm = value.into();
400                }
401                "server.thread.limit" => {
402                    if let Ok(value) = value.parse::<usize>() {
403                        server.thread_limit = Some(value);
404                    }
405                }
406                "server.request-timeout" => {
407                    // * = millisecond
408                    // *s = second
409                    // *m = minute
410                    let timeout = value.trim_end_matches(|c| !char::is_numeric(c));
411
412                    if let Ok(timeout) = timeout.parse::<u64>() {
413                        if timeout == 0 {
414                            continue;
415                        }
416
417                        let duration = match value.chars().last() {
418                            Some('s') => std::time::Duration::from_secs(timeout),
419                            Some('m') => std::time::Duration::from_secs(timeout * 60),
420                            _ => std::time::Duration::from_millis(timeout),
421                        };
422
423                        server.request_timeout = Some(duration);
424                    }
425                }
426                "server.request.uri.max-length" => {
427                    if let Ok(value) = value.parse::<usize>() {
428                        server.request.uri.max_length = Some(value);
429                    }
430                }
431                "server.request.header.max-length" => {
432                    if let Ok(value) = value.parse::<usize>() {
433                        server.request.header.max_length = Some(value);
434                    }
435                }
436                "server.request.header.max-number-of-headers" => {
437                    if let Ok(value) = value.parse::<usize>() {
438                        server.request.header.max_number_of_headers = Some(value);
439                    }
440                }
441                "server.request.body.max-length" => {
442                    if let Some(value) = parse_byte_size(value.as_str()) {
443                        server.request.body.max_length = value;
444                    }
445                }
446                "server.http1.keep-alive" => {
447                    if let Ok(value) = value.parse::<bool>() {
448                        server.http1.keep_alive = value;
449                    }
450                }
451                "server.ssl.key" => {
452                    server.ssl.key = value.to_string();
453                }
454                "server.multipart.auto-parsing-enabled" => {
455                    if let Ok(value) = value.parse::<bool>() {
456                        server.multipart.auto_parsing_enabled = value;
457                    }
458                }
459                "server.cookie.auto-parsing-enabled" => {
460                    if let Ok(value) = value.parse::<bool>() {
461                        server.cookie.auto_parsing_enabled = value;
462                    }
463                }
464                "server.ssl.cert" => {
465                    server.ssl.cert = value.to_string();
466                }
467                "environment" => {
468                    environment = value.to_string();
469                }
470                "banner.enabled" => {
471                    banner.enabled = value.parse::<bool>().unwrap_or(true);
472                }
473                "banner.location" => {
474                    banner.location = Some(value.to_string());
475                }
476                "banner.charset" => {
477                    banner.charset = value.to_string();
478                }
479                _ => {
480                    etc.insert(key, value);
481                }
482            }
483        }
484
485        ApplicationProperties {
486            server,
487            etc,
488            environment,
489            banner,
490        }
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    fn remove_all_env() {
499        for (key, _) in std::env::vars() {
500            std::env::remove_var(key);
501        }
502    }
503
504    #[test]
505    fn test_from_properties() {
506        struct TestCase {
507            name: String,
508            input: String,
509            before: fn() -> (),
510            expected: ApplicationProperties,
511        }
512
513        let test_cases = vec![
514            TestCase {
515                name: "일반적인 기본 속성 바인딩".to_string(),
516                input: r#"
517                    server.port=8080
518                    server.address=127.0.0.1
519                    "#
520                .to_string(),
521                expected: ApplicationProperties {
522                    server: Server {
523                        address: "127.0.0.1".to_string(),
524                        port: 8080,
525                        ..Default::default()
526                    },
527                    etc: HashMap::new(),
528                    environment: "dev".to_string(),
529                    ..Default::default()
530                },
531                before: || {
532                    remove_all_env();
533                },
534            },
535            TestCase {
536                name: "추가 속성 바인딩".to_string(),
537                input: r#"
538                    server.port=8080
539                    server.address=127.0.0.1
540                    foo.bar=hello
541                    "#
542                .to_string(),
543                expected: ApplicationProperties {
544                    server: Server {
545                        address: "127.0.0.1".to_string(),
546                        port: 8080,
547                        ..Default::default()
548                    },
549                    environment: "dev".to_string(),
550                    etc: HashMap::from([("foo.bar".to_string(), "hello".to_string())]),
551                    ..Default::default()
552                },
553                before: || {
554                    remove_all_env();
555                },
556            },
557            TestCase {
558                name: "추가 속성 바인딩 - 환경변수".to_string(),
559                input: r#"
560                    server.port=8080
561                    server.address=127.0.0.1
562                    "#
563                .to_string(),
564                expected: ApplicationProperties {
565                    server: Server {
566                        address: "127.0.0.1".to_string(),
567                        port: 8080,
568                        ..Default::default()
569                    },
570                    environment: "dev".to_string(),
571                    etc: HashMap::from([("asdf.fdsa".to_string(), "!!".to_string())]),
572                    ..Default::default()
573                },
574                before: || {
575                    remove_all_env();
576                    std::env::set_var("asdf.fdsa", "!!");
577                },
578            },
579            TestCase {
580                name: "따옴표로 감싸기".to_string(),
581                input: r#"
582                    server.port=8080
583                    server.address="127.0.0.1"
584                    "#
585                .to_string(),
586                expected: ApplicationProperties {
587                    server: Server {
588                        address: "127.0.0.1".to_string(),
589                        port: 8080,
590                        ..Default::default()
591                    },
592                    environment: "dev".to_string(),
593                    etc: HashMap::new(),
594                    ..Default::default()
595                },
596                before: || {
597                    remove_all_env();
598                },
599            },
600            TestCase {
601                name: "중간에 띄어쓰기".to_string(),
602                input: r#"
603                    server.port=8080
604                    server.address= 127.0.0.1
605                    "#
606                .to_string(),
607                expected: ApplicationProperties {
608                    server: Server {
609                        address: "127.0.0.1".to_string(),
610                        port: 8080,
611                        ..Default::default()
612                    },
613                    environment: "dev".to_string(),
614                    etc: HashMap::new(),
615                    ..Default::default()
616                },
617                before: || {
618                    remove_all_env();
619                },
620            },
621            TestCase {
622                name: "포트 파싱 실패".to_string(),
623                input: r#"
624                    server.port=80#@#@80
625                    server.address= 127.0.0.1
626                    "#
627                .to_string(),
628                expected: ApplicationProperties {
629                    server: Server {
630                        address: "127.0.0.1".to_string(),
631                        port: 3000,
632                        ..Default::default()
633                    },
634                    environment: "dev".to_string(),
635                    etc: HashMap::new(),
636                    ..Default::default()
637                },
638                before: || {
639                    remove_all_env();
640                },
641            },
642            TestCase {
643                name: "environment 바인딩".to_string(),
644                input: r#"
645                    server.port=80#@#@80
646                    server.address= 127.0.0.1
647                    environment=prod
648                    "#
649                .to_string(),
650                expected: ApplicationProperties {
651                    server: Server {
652                        address: "127.0.0.1".to_string(),
653                        port: 3000,
654                        ..Default::default()
655                    },
656                    environment: "prod".to_string(),
657                    etc: HashMap::new(),
658                    ..Default::default()
659                },
660                before: || {
661                    remove_all_env();
662                },
663            },
664        ];
665
666        for tc in test_cases {
667            (tc.before)();
668
669            let got = ApplicationProperties::from_properties(tc.input.clone());
670            assert_eq!(
671                got, tc.expected,
672                "{} - input: {:?}, actual: {:?}",
673                tc.name, tc.input, got
674            );
675        }
676    }
677}
678
679// 알아서 모든 대상에 대해 application.properties를 읽어서 ApplicationProperties를 반환하는 함수
680pub fn load_application_properties_from_all() -> ApplicationProperties {
681    // 1. 현재 경로에 application.properties가 있는지 확인하고, 있다면 읽어서 반환합니다.
682    if let Ok(text) = std::fs::read_to_string("application.properties") {
683        return ApplicationProperties::from_properties(text);
684    }
685
686    // 2. 실행파일 경로에 application.properties가 있는지 확인하고, 있다면 읽어서 반환합니다.
687    let exe_path = std::env::current_exe().expect("Failed to get current executable path");
688    let exe_dir = exe_path
689        .parent()
690        .expect("Failed to get executable directory");
691
692    let exe_properties_path = exe_dir.join("application.properties");
693    if let Ok(text) = std::fs::read_to_string(exe_properties_path) {
694        return ApplicationProperties::from_properties(text);
695    }
696
697    println!("application.properties Not Found. Use default properties.");
698
699    ApplicationProperties::default()
700}