mithril_build_script/
open_api.rs

1use semver::Version;
2use std::collections::BTreeMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6type OpenAPIFileName = String;
7type OpenAPIVersionRaw = String;
8
9const TYPE_ALIAS: &str = r"/// Open API file name
10pub type OpenAPIFileName = String;
11";
12
13fn list_all_open_api_spec_files(paths: &[&Path]) -> Vec<PathBuf> {
14    let mut open_api_spec_files = Vec::new();
15
16    for path in paths {
17        for entry in crate::list_files_in_folder(path).filter(|e| {
18            let os_filename = e.file_name();
19            let filename = os_filename.to_string_lossy();
20            filename.starts_with("openapi") && filename.ends_with(".yaml")
21        }) {
22            open_api_spec_files.push(entry.path())
23        }
24    }
25
26    open_api_spec_files
27}
28
29fn read_version_from_open_api_spec_file(spec_file_path: PathBuf) -> OpenAPIVersionRaw {
30    let yaml_spec = fs::read_to_string(spec_file_path).unwrap();
31    let open_api: serde_yaml::Value = serde_yaml::from_str(&yaml_spec).unwrap();
32    open_api["info"]["version"].as_str().unwrap().to_owned()
33}
34
35/// Generate the `get_open_api_versions_mapping` function based on the Open API files
36/// in the given folders.
37pub fn generate_open_api_versions_mapping(paths: &[&Path]) -> String {
38    let open_api_spec_files = list_all_open_api_spec_files(paths);
39    // Use a BTreeMap to guarantee the deterministic code generation below
40    let open_api_versions: BTreeMap<OpenAPIFileName, Version> = open_api_spec_files
41        .into_iter()
42        .map(|path| (path.clone(), read_version_from_open_api_spec_file(path)))
43        .map(|(path, version_raw)| {
44            (
45                path.file_name().unwrap().to_string_lossy().to_string(),
46                Version::parse(&version_raw).unwrap(),
47            )
48        })
49        .collect();
50
51    let mut open_api_versions_hashmap = String::new();
52    for (filename, version) in open_api_versions {
53        open_api_versions_hashmap.push_str(&format!(
54            r#"("{filename}".to_string(), semver::Version::new({}, {}, {})), "#,
55            version.major, version.minor, version.patch
56        ));
57    }
58
59    format!(
60        r#"{TYPE_ALIAS}
61/// Build Open API versions mapping
62pub fn get_open_api_versions_mapping() -> HashMap<OpenAPIFileName, semver::Version> {{
63    HashMap::from([
64        {}
65    ])
66}}
67        "#,
68        open_api_versions_hashmap
69    )
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::get_temp_dir;
76    use std::path::Path;
77
78    fn write_minimal_open_api_file(version: &str, path: &Path) {
79        fs::write(
80            path,
81            format!(
82                r#"openapi: "3.0.0"
83info:
84  version: {version}
85  title: Minimal Open Api File
86"#
87            ),
88        )
89        .unwrap()
90    }
91
92    fn assert_open_api_content_contains(expected_content: &str, generated_code: &str) {
93        assert!(
94            generated_code.contains(expected_content),
95            "generated code did not include expected openapi files entries:\
96            \n---- Code that was expected to be included:\n{expected_content}\
97            \n---- Actual generated code:{}",
98            // Remove type aliases for readability
99            generated_code.trim_start_matches(TYPE_ALIAS)
100        );
101    }
102
103    #[test]
104    fn generated_code_include_type_aliases() {
105        let generated_code = generate_open_api_versions_mapping(&[Path::new("./")]);
106
107        assert!(generated_code.contains(TYPE_ALIAS));
108    }
109
110    #[test]
111    fn generated_function_returns_an_hashmap_of_open_api_file_name_and_semver_version() {
112        let generated_code = generate_open_api_versions_mapping(&[Path::new("./")]);
113
114        assert!(generated_code.contains("-> HashMap<OpenAPIFileName, semver::Version>"));
115    }
116
117    #[test]
118    fn generate_code_from_a_simple_open_api_file() {
119        let dir = get_temp_dir("generate_code_from_a_simple_open_api_file");
120        write_minimal_open_api_file("1.0.0", &dir.join("openapi.yaml"));
121
122        let expected = r#"("openapi.yaml".to_string(), semver::Version::new(1, 0, 0))"#;
123        let generated_code = generate_open_api_versions_mapping(&[&dir]);
124
125        assert_open_api_content_contains(expected, &generated_code);
126    }
127
128    #[test]
129    fn only_read_yaml_files() {
130        let dir = get_temp_dir("only_read_yaml_files");
131        write_minimal_open_api_file("1.0.0", &dir.join("openapi.yaml"));
132        fs::write(dir.join("openapi.json"), "{}").unwrap();
133
134        let included_files = list_all_open_api_spec_files(&[&dir]);
135
136        assert_eq!(vec![dir.join("openapi.yaml")], included_files);
137    }
138
139    #[test]
140    fn generate_code_from_two_open_api_files_in_different_folders() {
141        let sub_folder =
142            get_temp_dir("generate_code_from_two_open_api_files_in_different_folders/subfolder");
143        let parent_folder = sub_folder.parent().unwrap();
144        write_minimal_open_api_file("1.0.0", &parent_folder.join("openapi.yaml"));
145        write_minimal_open_api_file("2.0.0", &sub_folder.join("openapi-thales.yaml"));
146
147        let expected = r#"("openapi-thales.yaml".to_string(), semver::Version::new(2, 0, 0)), ("openapi.yaml".to_string(), semver::Version::new(1, 0, 0))"#;
148        let generated_code = generate_open_api_versions_mapping(&[parent_folder, &sub_folder]);
149
150        assert_open_api_content_contains(expected, &generated_code);
151    }
152
153    #[test]
154    fn when_colliding_filenames_version_is_read_from_latest_given_folder() {
155        let sub_folder = get_temp_dir(
156            "when_colliding_filenames_version_read_is_from_latest_given_folder/subfolder",
157        );
158        let parent_folder = sub_folder.parent().unwrap();
159        write_minimal_open_api_file("1.0.0", &parent_folder.join("openapi.yaml"));
160        write_minimal_open_api_file("2.0.0", &sub_folder.join("openapi.yaml"));
161
162        let expected = r#"HashMap::from([
163        ("openapi.yaml".to_string(), semver::Version::new(2, 0, 0)), 
164    ])"#;
165        let generated_code = generate_open_api_versions_mapping(&[parent_folder, &sub_folder]);
166
167        assert_open_api_content_contains(expected, &generated_code);
168    }
169}