1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
//! Structures and tools to parse, navigate and validate [OpenAPI v3.1] specifications.
//!
//! # Example
//!
//! ```no_run
//! match oas3::from_path("path/to/openapi.yaml") {
//! Ok(spec) => println!("spec: {:?}", spec),
//! Err(err) => println!("error: {}", err)
//! }
//! ```
//!
//! [OpenAPI v3.1]: https://github.com/OAI/OpenAPI-Specification/blob/HEAD/versions/3.1.0.md
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(missing_debug_implementations)]
use std::{fs::File, io::Read, path::Path};
mod error;
pub mod spec;
pub use error::Error;
pub use spec::{Schema, Spec};
#[cfg(feature = "validation")]
pub mod validation;
#[cfg(feature = "conformance")]
pub mod conformance;
/// Version 3.1.0 of the OpenAPI specification.
///
/// Refer to the official [specification] for more information.
///
/// [specification]: https://github.com/OAI/OpenAPI-Specification/blob/HEAD/versions/3.1.0.md
pub type OpenApiV3Spec = spec::Spec;
/// Try deserializing an OpenAPI spec (YAML or JSON) from a file, giving the path.
pub fn from_path<P>(path: P) -> Result<OpenApiV3Spec, Error>
where
P: AsRef<Path>,
{
from_reader(File::open(path)?)
}
/// Try deserializing an OpenAPI spec (YAML or JSON) from a [`Read`] type.
pub fn from_reader<R>(read: R) -> Result<OpenApiV3Spec, Error>
where
R: Read,
{
Ok(serde_yaml::from_reader::<R, OpenApiV3Spec>(read)?)
}
/// Try serializing to a YAML string.
pub fn to_yaml(spec: &OpenApiV3Spec) -> Result<String, Error> {
Ok(serde_yaml::to_string(spec)?)
}
/// Try serializing to a JSON string.
pub fn to_json(spec: &OpenApiV3Spec) -> Result<String, Error> {
Ok(serde_json::to_string_pretty(spec)?)
}
#[cfg(test)]
mod tests {
use std::{
fs::{self, read_to_string, File},
io::Write,
path,
};
use pretty_assertions::assert_eq;
use super::*;
/// Helper function to write string to file.
fn write_to_file<P>(path: P, filename: &str, data: &str)
where
P: AsRef<Path> + std::fmt::Debug,
{
println!(" Saving string to {:?}...", path);
std::fs::create_dir_all(&path).unwrap();
let full_filename = path.as_ref().to_path_buf().join(filename);
let mut f = File::create(full_filename).unwrap();
f.write_all(data.as_bytes()).unwrap();
}
/// Convert a YAML `&str` to a JSON `String`.
fn convert_yaml_str_to_json(yaml_str: &str) -> String {
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let json: serde_json::Value = serde_yaml::from_value(yaml).unwrap();
serde_json::to_string_pretty(&json).unwrap()
}
/// Deserialize and re-serialize the input file to a JSON string through two different
/// paths, comparing the result.
/// 1. File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
/// 2. File -> `Spec` -> `serde_json::Value` -> `String`
///
/// Both conversion of `serde_json::Value` -> `String` are done
/// using `serde_json::to_string_pretty`.
/// Since the first conversion is independent of the current crate (and only
/// uses serde json and yaml support), no information should be lost in the final
/// JSON string. The second conversion goes through our `OpenApi`, so the final JSON
/// string is a representation of _our_ implementation.
/// By comparing those two JSON conversions, we can validate our implementation.
fn compare_spec_through_json(
input_file: &Path,
save_path_base: &Path,
) -> (String, String, String) {
// First conversion:
// File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
// Read the original file to string
let spec_yaml_str = read_to_string(input_file)
.unwrap_or_else(|e| panic!("failed to read contents of {:?}: {}", input_file, e));
// Convert YAML string to JSON string
let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str);
// Second conversion:
// File -> `Spec` -> `serde_json::Value` -> `String`
// Parse the input file
let parsed_spec = from_path(input_file).unwrap();
// Convert to serde_json::Value
let parsed_spec_json = serde_json::to_value(parsed_spec).unwrap();
// Convert to a JSON string
let parsed_spec_json_str: String = serde_json::to_string_pretty(&parsed_spec_json).unwrap();
// Save JSON strings to file
let api_filename = input_file
.file_name()
.unwrap()
.to_str()
.unwrap()
.replace(".yaml", ".json");
let mut save_path = save_path_base.to_path_buf();
save_path.push("yaml_to_json");
write_to_file(&save_path, &api_filename, &spec_json_str);
let mut save_path = save_path_base.to_path_buf();
save_path.push("yaml_to_spec_to_json");
write_to_file(&save_path, &api_filename, &parsed_spec_json_str);
// Return the JSON filename and the two JSON strings
(api_filename, parsed_spec_json_str, spec_json_str)
}
#[test]
#[ignore = "lib does not support all schema attributes yet"]
fn test_serialization_round_trip() {
let save_path_base: path::PathBuf = ["target", "tests", "test_serialization_round_trip"]
.iter()
.collect();
for entry in fs::read_dir("data/oas-samples").unwrap() {
let entry = entry.unwrap();
let path = entry.path();
println!("Testing if {:?} is deserializable", path);
let (api_filename, parsed_spec_json_str, spec_json_str) =
compare_spec_through_json(&path, &save_path_base);
assert_eq!(
parsed_spec_json_str.lines().collect::<Vec<_>>(),
spec_json_str.lines().collect::<Vec<_>>(),
"contents did not match for api {}",
api_filename
);
}
}
#[test]
fn test_json_from_reader() {
let yaml = r#"openapi: "3"
paths: {}
info:
title: Test API
version: "0.1"
components:
schemas:
assets:
title: Assets
type: array
items: { type: integer }"#;
let json = r#"{
"openapi": "3",
"paths": {},
"info": {
"title": "Test API",
"version": "0.1"
},
"components": {
"schemas": {
"assets": {
"title": "Assets",
"type": "array",
"items": {
"type": "integer"
}
}
}
}
}"#;
assert_eq!(
from_reader(json.as_bytes()).unwrap(),
from_reader(yaml.as_bytes()).unwrap()
);
}
}