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