apple_bloom/
lib.rs

1//! Openapi provides structures and support for serializing and deserializing [openapi](https://github.com/OAI/OpenAPI-Specification) specifications
2//!
3//! # Examples
4//!
5//! Typical use deserialing an existing to a persisted spec to rust form or
6//! visa versa
7//!
8//! The hyper client should be configured with tls.
9//!
10//! ```no_run
11//! extern crate apple_bloom;
12//!
13//! fn main() {
14//!   match apple_bloom::from_path("path/to/openapi.yaml") {
15//!     Ok(spec) => println!("spec: {:?}", spec),
16//!     Err(err) => println!("error: {}", err)
17//!   }
18//! }
19//! ```
20//!
21//! # Errors
22//!
23//! Operations typically result in a [`Result`] type, an alias for
24//! [`std::result::Result`] with the `Err` type fixed to [`Error`],
25//! which implements [`std::error::Error`].
26//!
27use serde::{Deserialize, Serialize};
28use std::{fs::File, io::Read, path::Path, result::Result as StdResult};
29
30pub mod error;
31pub mod v2;
32pub mod v3;
33
34pub use error::Error;
35
36const MINIMUM_OPENAPI30_VERSION: &str = ">= 3.0";
37
38pub type Result<T> = StdResult<T, Error>;
39
40/// Supported versions of the OpenApi.
41#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
42#[serde(untagged)]
43pub enum OpenApi {
44    /// Version 2.0 of the OpenApi specification.
45    ///
46    /// Refer to the official
47    /// [specification](https://github.com/OAI/OpenAPI-Specification/blob/0dd79f6/versions/2.0.md)
48    /// for more information.
49    V2(Box<v2::Spec>),
50
51    /// Version 3.0.1 of the OpenApi specification.
52    ///
53    /// Refer to the official
54    /// [specification](https://github.com/OAI/OpenAPI-Specification/blob/0dd79f6/versions/3.0.1.md)
55    /// for more information.
56    #[allow(non_camel_case_types)]
57    V3_0(Box<v3::Spec>),
58}
59
60/// deserialize an open api spec from a path
61pub fn from_path<P>(path: P) -> Result<OpenApi>
62where
63    P: AsRef<Path>,
64{
65    from_reader(File::open(path)?)
66}
67
68/// deserialize an open api spec from type which implements Read
69pub fn from_reader<R>(read: R) -> Result<OpenApi>
70where
71    R: Read,
72{
73    Ok(serde_yaml::from_reader::<R, OpenApi>(read)?)
74}
75
76/// serialize to a yaml string
77pub fn to_yaml(spec: &OpenApi) -> Result<String> {
78    Ok(serde_yaml::to_string(spec)?)
79}
80
81/// serialize to a json string
82pub fn to_json(spec: &OpenApi) -> Result<String> {
83    Ok(serde_json::to_string_pretty(spec)?)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use pretty_assertions::assert_eq;
90    use std::{
91        fs::{self, read_to_string, File},
92        io::Write,
93    };
94
95    /// Helper function to write string to file.
96    fn write_to_file<P>(path: P, filename: &str, data: &str)
97    where
98        P: AsRef<Path> + std::fmt::Debug,
99    {
100        println!("    Saving string to {:?}...", path);
101        std::fs::create_dir_all(&path).unwrap();
102        let full_filename = path.as_ref().to_path_buf().join(filename);
103        let mut f = File::create(&full_filename).unwrap();
104        f.write_all(data.as_bytes()).unwrap();
105    }
106
107    /// Convert a YAML `&str` to a JSON `String`.
108    fn convert_yaml_str_to_json(yaml_str: &str) -> String {
109        let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
110        let json: serde_json::Value = serde_yaml::from_value(yaml).unwrap();
111        serde_json::to_string_pretty(&json).unwrap()
112    }
113
114    /// Deserialize and re-serialize the input file to a JSON string through two different
115    /// paths, comparing the result.
116    /// 1. File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
117    /// 2. File -> `Spec` -> `serde_json::Value` -> `String`
118    /// Both conversion of `serde_json::Value` -> `String` are done
119    /// using `serde_json::to_string_pretty`.
120    /// Since the first conversion is independant of the current crate (and only
121    /// uses serde's json and yaml support), no information should be lost in the final
122    /// JSON string. The second conversion goes through our `OpenApi`, so the final JSON
123    /// string is a representation of _our_ implementation.
124    /// By comparing those two JSON conversions, we can validate our implementation.
125    fn compare_spec_through_json(
126        input_file: &Path,
127        save_path_base: &Path,
128    ) -> (String, String, String) {
129        // First conversion:
130        //     File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
131
132        // Read the original file to string
133        let spec_yaml_str = read_to_string(&input_file)
134            .unwrap_or_else(|e| panic!("failed to read contents of {:?}: {}", input_file, e));
135        // Convert YAML string to JSON string
136        let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str);
137
138        // Second conversion:
139        //     File -> `Spec` -> `serde_json::Value` -> `String`
140
141        // Parse the input file
142        let parsed_spec = from_path(&input_file).unwrap();
143        // Convert to serde_json::Value
144        let parsed_spec_json = serde_json::to_value(parsed_spec).unwrap();
145        // Convert to a JSON string
146        let parsed_spec_json_str: String = serde_json::to_string_pretty(&parsed_spec_json).unwrap();
147
148        // Save JSON strings to file
149        let api_filename = input_file
150            .file_name()
151            .unwrap()
152            .to_str()
153            .unwrap()
154            .replace(".yaml", ".json");
155
156        let mut save_path = save_path_base.to_path_buf();
157        save_path.push("yaml_to_json");
158        write_to_file(&save_path, &api_filename, &spec_json_str);
159
160        let mut save_path = save_path_base.to_path_buf();
161        save_path.push("yaml_to_spec_to_json");
162        write_to_file(&save_path, &api_filename, &parsed_spec_json_str);
163
164        // Return the JSON filename and the two JSON strings
165        (api_filename, parsed_spec_json_str, spec_json_str)
166    }
167
168    // Just tests if the deserialization does not blow up. But does not test correctness
169    #[test]
170    fn can_deserialize() {
171        for entry in fs::read_dir("data/v2").unwrap() {
172            let path = entry.unwrap().path();
173            // cargo test -- --nocapture to see this message
174            println!("Testing if {:?} is deserializable", path);
175            from_path(path).unwrap();
176        }
177    }
178
179    #[test]
180    fn can_deserialize_and_reserialize_v2() {
181        let save_path_base: std::path::PathBuf =
182            ["target", "tests", "can_deserialize_and_reserialize_v2"]
183                .iter()
184                .collect();
185
186        for entry in fs::read_dir("data/v2").unwrap() {
187            let path = entry.unwrap().path();
188
189            println!("Testing if {:?} is deserializable", path);
190
191            let (api_filename, parsed_spec_json_str, spec_json_str) =
192                compare_spec_through_json(&path, &save_path_base);
193
194            assert_eq!(
195                parsed_spec_json_str.lines().collect::<Vec<_>>(),
196                spec_json_str.lines().collect::<Vec<_>>(),
197                "contents did not match for api {}",
198                api_filename
199            );
200        }
201    }
202
203    #[test]
204    fn can_deserialize_and_reserialize_v3() {
205        let save_path_base: std::path::PathBuf =
206            ["target", "tests", "can_deserialize_and_reserialize_v3"]
207                .iter()
208                .collect();
209
210        for entry in fs::read_dir("data/v3.0").unwrap() {
211            let entry = entry.unwrap();
212            let path = entry.path();
213
214            println!("Testing if {:?} is deserializable", path);
215
216            let (api_filename, parsed_spec_json_str, spec_json_str) =
217                compare_spec_through_json(&path, &save_path_base);
218
219            assert_eq!(
220                parsed_spec_json_str.lines().collect::<Vec<_>>(),
221                spec_json_str.lines().collect::<Vec<_>>(),
222                "contents did not match for api {}",
223                api_filename
224            );
225        }
226    }
227
228    #[test]
229    fn can_deserialize_one_of_v3() {
230        let openapi = from_path("data/v3.0/petstore-expanded.yaml").unwrap();
231        if let OpenApi::V3_0(spec) = openapi {
232            let components = spec.components.unwrap();
233            let schemas = components.schemas.unwrap();
234            let obj_or_ref = schemas.get("PetSpecies");
235
236            if let Some(v3::ObjectOrReference::Object(schema)) = obj_or_ref {
237                // there should be 2 schemas in there
238                assert_eq!(schema.one_of.as_ref().unwrap().len(), 2);
239            } else {
240                panic!("object should have been schema");
241            }
242        }
243    }
244/*    #[test]
245    fn can_deserialize_freenas() {
246        // tests::can_deserialize_freenas' panicked at 'called `Result::unwrap()` on an `Err` value: Yaml(Message("data did not match any variant of untagged enum OpenApi", None))', src\lib.rs:252:23
247        let openapi = from_path("data/v3.0/FreeNAS_openapi.yaml").unwrap();
248        if let OpenApi::V3_0(spec) = openapi {
249            let components = spec.components.unwrap();
250            let schemas = components.schemas.unwrap();
251            let obj_or_ref = schemas.get("zfs_snapshot_rollback");
252
253            if obj_or_ref.is_none() {
254                panic!("object should have been an object");
255            }
256        }
257    }
258*/    
259    #[test]
260    fn can_deserialize_prtg() {
261        // tests::can_deserialize_freenas' panicked at 'called `Result::unwrap()` on an `Err` value: Yaml(Message("data did not match any variant of untagged enum OpenApi", None))', src\lib.rs:252:23
262        let openapi = from_path("data/v3.0/PRTG_openapi.yaml").unwrap();
263        if let OpenApi::V3_0(spec) = openapi {
264            let components = spec.components.unwrap();
265            let schemas = components.schemas.unwrap();
266            let obj_or_ref = schemas.get("prtg_status_warnings");
267
268            if obj_or_ref.is_none() {
269                panic!("object should have been an object");
270            }
271        }
272    }
273    
274}