postman_collection/
lib.rs

1use std::{fs::File, io::Read, path::Path};
2
3pub use errors::{Result, ResultExt};
4use serde::{Deserialize, Serialize};
5
6pub mod v1_0_0;
7pub mod v2_0_0;
8pub mod v2_1_0;
9
10const MINIMUM_POSTMAN_COLLECTION_VERSION: &str = ">= 1.0.0";
11
12/// Errors that Postman Collection functions may return
13pub mod errors {
14    use error_chain::error_chain;
15
16    use crate::MINIMUM_POSTMAN_COLLECTION_VERSION;
17
18    error_chain! {
19        foreign_links {
20            Io(::std::io::Error);
21            Yaml(::serde_yaml::Error);
22            Serialize(::serde_json::Error);
23            SemVerError(::semver::Error);
24        }
25
26        errors {
27            UnsupportedSpecFileVersion(version: ::semver::Version) {
28                description("Unsupported Postman Collection file version")
29                display("Unsupported Postman Collection file version ({}). Expected {}", version, MINIMUM_POSTMAN_COLLECTION_VERSION)
30            }
31        }
32    }
33}
34
35/// Supported versions of Postman Collection.
36#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
37#[serde(untagged)]
38pub enum PostmanCollection {
39    /// Version 1.0.0 of the Postman Collection specification.
40    ///
41    /// Refer to the official
42    /// [specification](https://schema.getpostman.com/collection/json/v1.0.0/draft-07/docs/index.html)
43    /// for more information.
44    #[allow(non_camel_case_types)]
45    V1_0_0(v1_0_0::Spec),
46    /// Version 1.0.0 of the Postman Collection specification.
47    ///
48    /// Refer to the official
49    /// [specification](https://schema.getpostman.com/collection/json/v2.0.0/draft-07/docs/index.html)
50    /// for more information.
51    #[allow(non_camel_case_types)]
52    V2_0_0(v2_0_0::Spec),
53    /// Version 1.0.0 of the Postman Collection specification.
54    ///
55    /// Refer to the official
56    /// [specification](https://schema.getpostman.com/collection/json/v2.1.0/draft-07/docs/index.html)
57    /// for more information.
58    #[allow(non_camel_case_types)]
59    V2_1_0(v2_1_0::Spec),
60}
61
62/// Deserialize a Postman Collection from a path
63pub fn from_path<P>(path: P) -> errors::Result<PostmanCollection>
64where
65    P: AsRef<Path>,
66{
67    from_reader(File::open(path)?)
68}
69
70/// Deserialize a Postman Collection from type which implements Read
71pub fn from_reader<R>(read: R) -> errors::Result<PostmanCollection>
72where
73    R: Read,
74{
75    Ok(serde_yaml::from_reader::<R, PostmanCollection>(read)?)
76}
77
78/// Serialize Postman Collection spec to a YAML string
79pub fn to_yaml(spec: &PostmanCollection) -> errors::Result<String> {
80    Ok(serde_yaml::to_string(spec)?)
81}
82
83/// Serialize Postman Collection spec to JSON string
84pub fn to_json(spec: &PostmanCollection) -> errors::Result<String> {
85    Ok(serde_json::to_string_pretty(spec)?)
86}
87
88#[cfg(test)]
89mod tests {
90    use std::fs::File;
91    use std::io::Write;
92
93    use glob::glob;
94
95    use super::*;
96
97    /// Helper function for reading a file to string.
98    fn read_file<P>(path: P) -> String
99    where
100        P: AsRef<Path>,
101    {
102        let mut f = File::open(path).unwrap();
103        let mut content = String::new();
104        f.read_to_string(&mut content).unwrap();
105        content
106    }
107
108    /// Helper function to write string to file.
109    fn write_to_file<P>(path: P, filename: &str, data: &str)
110    where
111        P: AsRef<Path> + std::fmt::Debug,
112    {
113        println!("    Saving string to {:?}...", path);
114        std::fs::create_dir_all(&path).unwrap();
115        let full_filename = path.as_ref().to_path_buf().join(filename);
116        let mut f = File::create(&full_filename).unwrap();
117        f.write_all(data.as_bytes()).unwrap();
118    }
119
120    /// Convert a YAML `&str` to a JSON `String`.
121    fn convert_yaml_str_to_json(yaml_str: &str) -> String {
122        let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
123        let json: serde_json::Value = serde_yaml::from_value(yaml).unwrap();
124        serde_json::to_string_pretty(&json).unwrap()
125    }
126
127    /// Deserialize and re-serialize the input file to a JSON string through two different
128    /// paths, comparing the result.
129    /// 1. File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
130    /// 2. File -> `Spec` -> `serde_json::Value` -> `String`
131    /// Both conversion of `serde_json::Value` -> `String` are done
132    /// using `serde_json::to_string_pretty`.
133    /// Since the first conversion is independent of the current crate (and only
134    /// uses serde's json and yaml support), no information should be lost in the final
135    /// JSON string. The second conversion goes through our `PostmanCollection`, so the final JSON
136    /// string is a representation of _our_ implementation.
137    /// By comparing those two JSON conversions, we can validate our implementation.
138    fn compare_spec_through_json(
139        input_file: &Path,
140        save_path_base: &Path,
141    ) -> (String, String, String) {
142        // First conversion:
143        //     File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
144
145        // Read the original file to string
146        let spec_yaml_str = read_file(&input_file);
147        // Convert YAML string to JSON string
148        let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str);
149
150        // Second conversion:
151        //     File -> `Spec` -> `serde_json::Value` -> `String`
152
153        // Parse the input file
154        let parsed_spec = from_path(&input_file).unwrap();
155        // Convert to serde_json::Value
156        let parsed_spec_json: serde_json::Value = serde_json::to_value(parsed_spec).unwrap();
157        // Convert to a JSON string
158        let parsed_spec_json_str: String = serde_json::to_string_pretty(&parsed_spec_json).unwrap();
159
160        // Save JSON strings to file
161        let api_filename = input_file
162            .file_name()
163            .unwrap()
164            .to_str()
165            .unwrap()
166            .replace(".yaml", ".json");
167
168        let mut save_path = save_path_base.to_path_buf();
169        save_path.push("yaml_to_json");
170        write_to_file(&save_path, &api_filename, &spec_json_str);
171
172        let mut save_path = save_path_base.to_path_buf();
173        save_path.push("yaml_to_spec_to_json");
174        write_to_file(&save_path, &api_filename, &parsed_spec_json_str);
175
176        // Return the JSON filename and the two JSON strings
177        (api_filename, parsed_spec_json_str, spec_json_str)
178    }
179
180    // Just tests if the deserialization does not blow up. But does not test correctness
181    #[test]
182    fn can_deserialize() {
183        for entry in glob("/tests/fixtures/collection/*.json").expect("Failed to read glob pattern")
184        {
185            let entry = entry.unwrap();
186            let path = entry.as_path();
187            // cargo test -- --nocapture to see this message
188            println!("Testing if {:?} is deserializable", path);
189            from_path(path).unwrap();
190        }
191    }
192
193    #[test]
194    fn can_deserialize_and_reserialize() {
195        let save_path_base: std::path::PathBuf =
196            ["target", "tests", "can_deserialize_and_reserialize"]
197                .iter()
198                .collect();
199        let mut invalid_diffs = Vec::new();
200
201        for entry in glob("/tests/fixtures/collection/*.json").expect("Failed to read glob pattern")
202        {
203            let entry = entry.unwrap();
204            let path = entry.as_path();
205
206            println!("Testing if {:?} is deserializable", path);
207
208            let (api_filename, parsed_spec_json_str, spec_json_str) =
209                compare_spec_through_json(path, &save_path_base);
210
211            if parsed_spec_json_str != spec_json_str {
212                invalid_diffs.push((api_filename, parsed_spec_json_str, spec_json_str));
213            }
214        }
215
216        for invalid_diff in &invalid_diffs {
217            println!("File {} failed JSON comparison!", invalid_diff.0);
218        }
219        assert_eq!(invalid_diffs.len(), 0);
220    }
221}