Skip to main content

tiny_proxy/config/
parser.rs

1use crate::config::{Config, Directive, SiteConfig};
2use crate::error::ProxyError;
3use std::collections::HashMap;
4use std::str::FromStr;
5
6#[derive(Debug)]
7struct PendingBlock {
8    directive_type: String, // "handle_path", "method", etc.
9    args: Vec<String>,      // Arguments, e.g., ["/api/*"] for handle_path
10}
11
12impl Config {
13    pub fn from_file(path: &str) -> Result<Self, ProxyError> {
14        let content = std::fs::read_to_string(path)?;
15        content.parse()
16    }
17}
18
19impl FromStr for Config {
20    type Err = ProxyError;
21
22    fn from_str(content: &str) -> Result<Self, Self::Err> {
23        let mut sites = HashMap::new();
24        let mut current_site_address: Option<String> = None;
25
26        // Stack for storing directives. Each vector element is a list of directives for the current nesting level.
27        // Initially we have one level - the site level.
28        let mut directive_stack: Vec<Vec<Directive>> = vec![vec![]];
29
30        // Stack for storing information about the blocks we are currently parsing
31        let mut block_stack: Vec<PendingBlock> = vec![];
32
33        for (line_num, raw_line) in content.lines().enumerate() {
34            let line = raw_line.trim();
35            if line.is_empty() || line.starts_with('#') {
36                continue;
37            }
38
39            // 1. Handle opening brace
40            if line.ends_with('{') {
41                let parts: Vec<&str> = line.split_whitespace().collect();
42                if parts.is_empty() {
43                    continue;
44                }
45
46                // If we are at the top level, this could be the start of a site block
47                if directive_stack.len() == 1 && current_site_address.is_none() {
48                    current_site_address = Some(parts[0].to_string());
49                    continue;
50                }
51
52                // Otherwise this is the start of a nested block (handle_path, method, etc.)
53                let directive_type = parts[0].to_string();
54                let args = parts[1..].iter().map(|s| s.to_string()).collect();
55
56                block_stack.push(PendingBlock {
57                    directive_type,
58                    args,
59                });
60                directive_stack.push(vec![]); // Add a new level for nested directives
61                continue;
62            }
63
64            // 2. Handle closing brace
65            if line == "}" {
66                if directive_stack.len() > 1 {
67                    let finished_directives = directive_stack.pop().unwrap();
68                    let block_info = block_stack.pop().unwrap();
69
70                    let completed_directive = match block_info.directive_type.as_str() {
71                        "handle_path" => {
72                            let pattern = block_info.args.first().cloned().unwrap_or_default();
73                            Directive::HandlePath {
74                                pattern,
75                                directives: finished_directives,
76                            }
77                        }
78                        "method" => Directive::Method {
79                            methods: block_info.args,
80                            directives: finished_directives,
81                        },
82                        _ => {
83                            return Err(ProxyError::Parse(format!(
84                                "Unknown block type: {}",
85                                block_info.directive_type
86                            )))
87                        }
88                    };
89
90                    // Add the assembled directive to the level above
91                    directive_stack
92                        .last_mut()
93                        .unwrap()
94                        .push(completed_directive);
95                } else {
96                    // Site block closed
97                    if let Some(address) = current_site_address.take() {
98                        let site_directives = directive_stack.pop().unwrap();
99                        sites.insert(
100                            address.clone(),
101                            SiteConfig {
102                                address,
103                                directives: site_directives,
104                            },
105                        );
106                        directive_stack.push(vec![]); // Prepare vector for the next site
107                    }
108                }
109                continue;
110            }
111
112            // 3. Handle simple directives (single line)
113            let parts: Vec<&str> = line.split_whitespace().collect();
114            if parts.is_empty() {
115                continue;
116            }
117
118            let directive_name = parts[0];
119            let args = parts[1..].to_vec();
120
121            let directive = match directive_name {
122                "reverse_proxy" => {
123                    let to = args.first().cloned().ok_or_else(|| {
124                        ProxyError::Parse("Missing backend URL for reverse_proxy".to_string())
125                    })?;
126                    Directive::ReverseProxy { to: to.to_string() }
127                }
128                "uri_replace" => {
129                    let find = args.first().cloned().ok_or_else(|| {
130                        ProxyError::Parse("Missing 'find' arg for uri_replace".to_string())
131                    })?;
132                    let replace = args.get(1).cloned().ok_or_else(|| {
133                        ProxyError::Parse("Missing 'replace' arg for uri_replace".to_string())
134                    })?;
135                    Directive::UriReplace {
136                        find: find.to_string(),
137                        replace: replace.to_string(),
138                    }
139                }
140                "header" => {
141                    let name = args.first().cloned().ok_or_else(|| {
142                        ProxyError::Parse("Missing 'name' arg for header".to_string())
143                    })?;
144                    let value = args.get(1).cloned().ok_or_else(|| {
145                        ProxyError::Parse("Missing 'value' arg for header".to_string())
146                    })?;
147                    Directive::Header {
148                        name: name.to_string(),
149                        value: value.to_string(),
150                    }
151                }
152                "respond" => {
153                    let status = args.first().and_then(|s| s.parse().ok()).ok_or_else(|| {
154                        ProxyError::Parse("Invalid status for respond".to_string())
155                    })?;
156                    let body = args.get(1).cloned().unwrap_or_default();
157                    Directive::Respond {
158                        status,
159                        body: body.to_string(),
160                    }
161                }
162                _ => {
163                    return Err(ProxyError::Parse(format!(
164                        "Unknown directive '{}' on line {}",
165                        directive_name,
166                        line_num + 1
167                    )))
168                }
169            };
170
171            // Add the directive to the current nesting level
172            directive_stack.last_mut().unwrap().push(directive);
173        }
174
175        Ok(Config { sites })
176    }
177}