tfdoc/
parser.rs

1use std::fs::File;
2use std::io::prelude::*;
3use std::io::BufReader;
4use std::path::PathBuf;
5
6use crate::types::{BlockType, DocItem};
7
8#[derive(PartialEq)]
9enum Directive {
10    Continue,
11    Stop,
12}
13
14pub fn parse_hcl(filename: PathBuf) -> std::io::Result<Vec<DocItem>> {
15    let file = File::open(filename)?;
16    let buf_reader = BufReader::new(file);
17    let mut result = vec![DocItem::new()];
18    for line in buf_reader.lines() {
19        let state = parse_line(line?, result.pop().unwrap());
20        result.push(state.0);
21        if state.1 == Directive::Stop {
22            result.push(DocItem::new());
23        }
24    }
25    result.pop(); // Remove the last DocItem from the collection since it's empty
26    result = result
27        .into_iter()
28        .filter(|i| {
29            if i.category == BlockType::Comment {
30                if let Some(line) = i.description.first() {
31                    return line.starts_with("Title: ");
32                }
33            }
34            true
35        })
36        .collect();
37    Ok(result)
38}
39
40fn parse_line(line: String, mut result: DocItem) -> (DocItem, Directive) {
41    match get_line_variant(&line) {
42        BlockType::Resource => parse_regular(line, result, BlockType::Resource, &parse_resource),
43        BlockType::Output => parse_regular(line, result, BlockType::Output, &parse_interface),
44        BlockType::Variable => parse_regular(line, result, BlockType::Variable, &parse_interface),
45        BlockType::Comment => (parse_comment(line, result), Directive::Continue),
46        BlockType::None => {
47            // Determine if it should stop parsing this block
48            if (line.starts_with('}') && result.category != BlockType::None)
49                || (line.trim().len() == 0 && result.category == BlockType::Comment)
50            {
51                return (result, Directive::Stop);
52            }
53            // Parse description if relevant
54            if (result.category == BlockType::Variable || result.category == BlockType::Output)
55                && line.trim().starts_with("description")
56            {
57                if let Some(description) = parse_description(&line) {
58                    result.description.push(String::from(description));
59                }
60            }
61            (result, Directive::Continue)
62        }
63    }
64}
65
66fn get_line_variant(line: &str) -> BlockType {
67    let variants = vec![
68        ("resource ", BlockType::Resource),
69        ("variable ", BlockType::Variable),
70        ("output ", BlockType::Output),
71        ("#", BlockType::Comment),
72    ];
73    for variant in variants {
74        if line.starts_with(variant.0) {
75            return variant.1;
76        }
77    }
78    BlockType::None
79}
80
81fn parse_regular(
82    line: String,
83    mut result: DocItem,
84    category: BlockType,
85    parser_function: &Fn(&str) -> String,
86) -> (DocItem, Directive) {
87    result.category = category;
88    result.name = parser_function(&line);
89    match line.trim().ends_with('}') {
90        true => (result, Directive::Stop),
91        false => (result, Directive::Continue),
92    }
93}
94
95fn parse_resource(line: &str) -> String {
96    line.split_whitespace()
97        .skip(1)
98        .take(2)
99        .map(|s| s.trim_matches('"'))
100        .collect::<Vec<&str>>()
101        .join(".")
102}
103
104fn parse_interface(line: &str) -> String {
105    // Parse variable and output blocks
106    let result = line
107        .split_whitespace()
108        .skip(1)
109        .take(1)
110        .map(|s| s.trim_matches('"'))
111        .collect::<Vec<&str>>()[0];
112    String::from(result)
113}
114
115fn parse_description(line: &str) -> Option<&str> {
116    let start = line.find('"')?;
117    let substring = line.get(start..)?;
118    Some(substring.trim_matches('"'))
119}
120
121fn parse_comment(line: String, mut result: DocItem) -> DocItem {
122    let parsed = line.trim_start_matches('#').trim();
123    if parsed.len() > 0 {
124        result.category = BlockType::Comment;
125        result.description.push(String::from(parsed));
126    }
127    result
128}
129
130//
131// Unit tests
132//
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn get_line_variant_resource() {
140        let line = r#"resource "foo" "bar" {"#;
141        match get_line_variant(line) {
142            BlockType::Resource => {}
143            _ => panic!("Type error!"),
144        }
145    }
146
147    #[test]
148    fn get_line_variant_output() {
149        let line = r#"output "foo" {"#;
150        match get_line_variant(line) {
151            BlockType::Output => {}
152            _ => panic!("Type error!"),
153        }
154    }
155
156    #[test]
157    fn get_line_variant_variable() {
158        let line = r#"variable "foo" {"#;
159        match get_line_variant(line) {
160            BlockType::Variable => {}
161            _ => panic!("Type error!"),
162        }
163    }
164
165    #[test]
166    fn get_line_variant_comment() {
167        let line = r#"# foo"#;
168        match get_line_variant(line) {
169            BlockType::Comment => {}
170            _ => panic!("Type error!"),
171        }
172    }
173
174    #[test]
175    fn get_line_variant_comment2() {
176        let line = r#"#foo"#;
177        match get_line_variant(line) {
178            BlockType::Comment => {}
179            _ => panic!("Type error!"),
180        }
181    }
182
183    #[test]
184    fn get_line_variant_none() {
185        let line = r#"  foo"#;
186        match get_line_variant(line) {
187            BlockType::None => {}
188            _ => panic!("Type error!"),
189        }
190    }
191
192    #[test]
193    fn test_parse_resource() {
194        let line = r#"resource "foo" "bar" {"#;
195        assert_eq!(parse_resource(line), "foo.bar".to_string());
196    }
197
198    #[test]
199    fn test_parse_output() {
200        let line = r#"output "foo" {"#;
201        assert_eq!(parse_interface(line), "foo");
202    }
203
204    #[test]
205    fn test_parse_variable() {
206        let line = r#"variable "foo" {"#;
207        assert_eq!(parse_interface(line), "foo");
208    }
209
210    #[test]
211    fn test_parse_description() {
212        let line = r#"  description = "foo bar""#;
213        assert_eq!(parse_description(line), Some("foo bar"));
214    }
215
216    #[test]
217    fn test_parse_comment() {
218        let line = String::from(r#"# foo bar"#);
219        let result = DocItem::new();
220        assert_eq!(parse_comment(line, result).description[0], "foo bar");
221    }
222
223    #[test]
224    fn test_parse_comment2() {
225        let line = String::from(r#"#foo bar"#);
226        let result = DocItem::new();
227        assert_eq!(parse_comment(line, result).description[0], "foo bar");
228    }
229}