openapi_rocketapi/
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 openapi;
12//!
13//! fn main() {
14//!   match openapi::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 `openapi::Result` Type which is an alias
24//! for Rust's
25//! built-in Result with the Err Type fixed to the
26//! [openapi::errors::Error](errors/struct.Error.html) enum type. These are
27//! provided
28//! using [error_chain](https://github.com/brson/error-chain) crate so their
29//! shape and behavior should be consistent and familiar to existing
30//! error_chain users.
31//!
32use serde::{Deserialize, Serialize};
33use std::{fs::File, io::Read, path::Path, result::Result as StdResult};
34
35pub mod error;
36pub mod v2;
37pub mod v3_0;
38
39pub use error::Error;
40
41const MINIMUM_OPENAPI30_VERSION: &str = ">= 3.0";
42
43pub type Result<T> = StdResult<T, Error>;
44
45/// Supported versions of the OpenApi.
46#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
47#[serde(untagged)]
48pub enum OpenApi {
49    /// Version 2.0 of the OpenApi specification.
50    ///
51    /// Refer to the official
52    /// [specification](https://github.com/OAI/OpenAPI-Specification/blob/0dd79f6/versions/2.0.md)
53    /// for more information.
54    V2(v2::Spec),
55
56    /// Version 3.0.1 of the OpenApi specification.
57    ///
58    /// Refer to the official
59    /// [specification](https://github.com/OAI/OpenAPI-Specification/blob/0dd79f6/versions/3.0.1.md)
60    /// for more information.
61    #[allow(non_camel_case_types)]
62    V3_0(v3_0::Spec),
63}
64
65/// deserialize an open api spec from a path
66pub fn from_path<P>(path: P) -> Result<OpenApi>
67where
68    P: AsRef<Path>,
69{
70    from_reader(File::open(path)?)
71}
72
73/// deserialize an open api spec from type which implements Read
74pub fn from_reader<R>(read: R) -> Result<OpenApi>
75where
76    R: Read,
77{
78    Ok(serde_yaml::from_reader::<R, OpenApi>(read)?)
79}
80
81/// serialize to a yaml string
82pub fn to_yaml(spec: &OpenApi) -> Result<String> {
83    Ok(serde_yaml::to_string(spec)?)
84}
85
86/// serialize to a json string
87pub fn to_json(spec: &OpenApi) -> Result<String> {
88    Ok(serde_json::to_string_pretty(spec)?)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use pretty_assertions::assert_eq;
95    use std::{
96        fs::{self, read_to_string, File},
97        io::Write,
98    };
99
100    /// Helper function to write string to file.
101    fn write_to_file<P>(
102        path: P,
103        filename: &str,
104        data: &str,
105    ) where
106        P: AsRef<Path> + std::fmt::Debug,
107    {
108        println!("    Saving string to {:?}...", path);
109        std::fs::create_dir_all(&path).unwrap();
110        let full_filename = path.as_ref().to_path_buf().join(filename);
111        let mut f = File::create(&full_filename).unwrap();
112        f.write_all(data.as_bytes()).unwrap();
113    }
114
115    /// Convert a YAML `&str` to a JSON `String`.
116    fn convert_yaml_str_to_json(yaml_str: &str) -> String {
117        let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
118        let json: serde_json::Value = serde_yaml::from_value(yaml).unwrap();
119        serde_json::to_string_pretty(&json).unwrap()
120    }
121
122    /// Deserialize and re-serialize the input file to a JSON string through two different
123    /// paths, comparing the result.
124    /// 1. File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
125    /// 2. File -> `Spec` -> `serde_json::Value` -> `String`
126    /// Both conversion of `serde_json::Value` -> `String` are done
127    /// using `serde_json::to_string_pretty`.
128    /// Since the first conversion is independant of the current crate (and only
129    /// uses serde's json and yaml support), no information should be lost in the final
130    /// JSON string. The second conversion goes through our `OpenApi`, so the final JSON
131    /// string is a representation of _our_ implementation.
132    /// By comparing those two JSON conversions, we can validate our implementation.
133    fn compare_spec_through_json(
134        input_file: &Path,
135        save_path_base: &Path,
136    ) -> (String, String, String) {
137        // First conversion:
138        //     File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
139
140        // Read the original file to string
141        let spec_yaml_str = read_to_string(&input_file)
142            .unwrap_or_else(|e| panic!("failed to read contents of {:?}: {}", input_file, e));
143        // Convert YAML string to JSON string
144        let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str);
145
146        // Second conversion:
147        //     File -> `Spec` -> `serde_json::Value` -> `String`
148
149        // Parse the input file
150        let parsed_spec = from_path(&input_file).unwrap();
151        // Convert to serde_json::Value
152        let parsed_spec_json = serde_json::to_value(parsed_spec).unwrap();
153        // Convert to a JSON string
154        let parsed_spec_json_str: String = serde_json::to_string_pretty(&parsed_spec_json).unwrap();
155
156        // Save JSON strings to file
157        let api_filename = input_file
158            .file_name()
159            .unwrap()
160            .to_str()
161            .unwrap()
162            .replace(".yaml", ".json");
163
164        let mut save_path = save_path_base.to_path_buf();
165        save_path.push("yaml_to_json");
166        write_to_file(&save_path, &api_filename, &spec_json_str);
167
168        let mut save_path = save_path_base.to_path_buf();
169        save_path.push("yaml_to_spec_to_json");
170        write_to_file(&save_path, &api_filename, &parsed_spec_json_str);
171
172        // Return the JSON filename and the two JSON strings
173        (api_filename, parsed_spec_json_str, spec_json_str)
174    }
175
176    // Just tests if the deserialization does not blow up. But does not test correctness
177    #[test]
178    fn can_deserialize() {
179        for entry in fs::read_dir("data/v2").unwrap() {
180            let path = entry.unwrap().path();
181            // cargo test -- --nocapture to see this message
182            println!("Testing if {:?} is deserializable", path);
183            from_path(path).unwrap();
184        }
185    }
186
187    #[test]
188    fn can_deserialize_and_reserialize_v2() {
189        let save_path_base: std::path::PathBuf =
190            ["target", "tests", "can_deserialize_and_reserialize_v2"]
191                .iter()
192                .collect();
193
194        for entry in fs::read_dir("data/v2").unwrap() {
195            let path = entry.unwrap().path();
196
197            println!("Testing if {:?} is deserializable", path);
198
199            let (api_filename, parsed_spec_json_str, spec_json_str) =
200                compare_spec_through_json(&path, &save_path_base);
201
202            assert_eq!(
203                parsed_spec_json_str.lines().collect::<Vec<_>>(),
204                spec_json_str.lines().collect::<Vec<_>>(),
205                "contents did not match for api {}",
206                api_filename
207            );
208        }
209    }
210
211    #[test]
212    fn can_deserialize_and_reserialize_v3() {
213        let save_path_base: std::path::PathBuf =
214            ["target", "tests", "can_deserialize_and_reserialize_v3"]
215                .iter()
216                .collect();
217
218        for entry in fs::read_dir("data/v3.0").unwrap() {
219            let entry = entry.unwrap();
220            let path = entry.path();
221
222            println!("Testing if {:?} is deserializable", path);
223
224            let (api_filename, parsed_spec_json_str, spec_json_str) =
225                compare_spec_through_json(&path, &save_path_base);
226
227            assert_eq!(
228                parsed_spec_json_str.lines().collect::<Vec<_>>(),
229                spec_json_str.lines().collect::<Vec<_>>(),
230                "contents did not match for api {}",
231                api_filename
232            );
233        }
234    }
235}