use anyhow::{format_err, Context, Error, Result};
use log::debug;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{serde_as, DisplayFromStr};
use std::io;
use std::path::PathBuf;
use std::{collections::BTreeMap, fs, path::Path, sync::Arc};
pub mod included_files;
mod interface;
mod ref_or;
mod schema;
mod serde_helpers;
mod set_or_scalar;
mod transpile;
use crate::parse_error::{Annotation, FileInfo, ParseError};
use self::interface::Interfaces;
use self::ref_or::{ExpectedWhenParsing, RefOr};
use self::schema::Schema;
pub use self::transpile::{Scope, Transpile};
#[serde_as]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenApi {
#[serde_as(as = "DisplayFromStr")]
openapi: Version,
#[serde(
rename = "$includeFiles",
default,
skip_serializing_if = "Vec::is_empty"
)]
include_files: Vec<PathBuf>,
#[serde(default)]
paths: BTreeMap<String, BTreeMap<Method, Operation>>,
#[serde(default)]
components: Components,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl OpenApi {
pub fn from_path(path: &Path) -> Result<Self> {
let display_path = path.display().to_string();
let contents = fs::read_to_string(path)
.with_context(|| format!("error reading {}", display_path))?;
let api =
serde_yaml::from_str::<OpenApi>(&contents).map_err(|err| -> Error {
let mut annotations = vec![];
if let Some(loc) = err.location() {
annotations
.push(Annotation::primary(loc.index(), "error occurred here"));
}
let parse_error = ParseError::new(
Arc::new(FileInfo::new(display_path, contents)),
annotations,
err.to_string(),
);
debug!("parse error: {}", parse_error);
parse_error.into()
})?;
let vers_req = VersionReq::parse("^3.0").unwrap();
if vers_req.matches(&api.openapi) {
Ok(api)
} else {
Err(format_err!("OpenAPI 3.x supported, found {}", &api.openapi))
}
}
pub fn supports_type_null(&self) -> bool {
let vers_req = VersionReq::parse("^3.1").unwrap();
vers_req.matches(&self.openapi)
}
pub fn to_writer(&self, writer: &mut dyn io::Write) -> Result<()> {
writeln!(writer, "# AUTOMATICALLY GENERATED. DO NOT EDIT.")?;
serde_yaml::to_writer(writer, self).map_err(|e| e.into())
}
}
impl Transpile for OpenApi {
type Output = Self;
fn transpile(&self, scope: &Scope) -> Result<Self> {
let openapi =
if scope.use_nullable_for_merge_patch && self.supports_type_null() {
Version::new(3, 0, 0)
} else {
self.openapi.clone()
};
Ok(Self {
openapi,
include_files: Default::default(),
paths: self.paths.transpile(scope)?,
components: self.components.transpile(scope)?,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct Components {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
responses: BTreeMap<String, ResponseBody>,
#[serde(default)]
schemas: BTreeMap<String, Schema>,
#[serde(default, skip_serializing_if = "Interfaces::is_empty")]
interfaces: Interfaces,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Transpile for Components {
type Output = Self;
fn transpile(&self, scope: &Scope) -> Result<Self> {
let responses = self.responses.transpile(scope)?;
let mut schemas = self.schemas.transpile(scope)?;
let interface_schemas = self.interfaces.transpile(scope)?;
for (name, schema) in interface_schemas {
if schemas.insert(name.clone(), schema).is_some() {
return Err(format_err!(
"existing schema {:?} would be overwritten by a generated schema",
name
));
}
}
Ok(Self {
responses,
schemas,
interfaces: Interfaces::default(),
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "lowercase")]
#[allow(clippy::missing_docs_in_private_items)]
enum Method {
Connect,
Delete,
Get,
Head,
Options,
Patch,
Post,
Put,
Trace,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct Operation {
#[serde(skip_serializing_if = "Option::is_none")]
request_body: Option<RequestBody>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
responses: BTreeMap<u16, RefOr<ResponseBody>>,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Transpile for Operation {
type Output = Self;
fn transpile(&self, scope: &Scope) -> Result<Self> {
Ok(Self {
request_body: self.request_body.transpile(scope)?,
responses: self.responses.transpile(scope)?,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[test]
fn deserializes_operation_responses_without_refs() {
let yaml = r#"
responses:
200:
description: A list of datasets
content:
application/json:
schema:
type: array
items:
$interface: "Dataset"
"#;
serde_yaml::from_str::<Operation>(yaml).unwrap();
}
#[test]
fn deserializes_operation_responses_with_refs() {
let yaml = r##"
responses:
200:
$ref: "#/components/responses/Ok"
"##;
serde_yaml::from_str::<Operation>(yaml).unwrap();
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct RequestBody {
content: BTreeMap<String, MediaType>,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Transpile for RequestBody {
type Output = Self;
fn transpile(&self, scope: &Scope) -> Result<Self> {
Ok(Self {
content: self.content.transpile(scope)?,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ResponseBody {
content: BTreeMap<String, MediaType>,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl ExpectedWhenParsing for ResponseBody {
fn expected_when_parsing() -> &'static str {
"a response body definition"
}
}
impl Transpile for ResponseBody {
type Output = Self;
fn transpile(&self, scope: &Scope) -> Result<Self> {
Ok(Self {
content: self.content.transpile(scope)?,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct MediaType {
schema: Schema,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Transpile for MediaType {
type Output = Self;
fn transpile(&self, scope: &Scope) -> Result<Self> {
Ok(Self {
schema: self.schema.transpile(scope)?,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[test]
fn parses_example() {
use pretty_assertions::assert_eq;
let path = Path::new("./examples/example.yml").to_owned();
let parsed = OpenApi::from_path(&path).unwrap();
let transpiled = parsed.transpile(&Scope::default()).unwrap();
let expected =
OpenApi::from_path(Path::new("./examples/example_output.yml")).unwrap();
assert_eq!(transpiled, expected);
}
#[test]
fn parses_long_example() {
use pretty_assertions::assert_eq;
let path = Path::new("./examples/long_example.yml").to_owned();
let parsed = OpenApi::from_path(&path).unwrap();
let transpiled = parsed.transpile(&Scope::default()).unwrap();
let expected =
OpenApi::from_path(Path::new("./examples/long_example_output.yml")).unwrap();
assert_eq!(transpiled, expected);
}
#[test]
fn parses_include_file_example() {
use crate::openapi::included_files::resolve_included_files;
use pretty_assertions::assert_eq;
let path = Path::new("./examples/include_files/base.yml").to_owned();
let mut parsed = OpenApi::from_path(&path).unwrap();
resolve_included_files(&mut parsed, &path).unwrap();
let transpiled = parsed.transpile(&Scope::default()).unwrap();
let expected =
OpenApi::from_path(Path::new("./examples/include_files/output.yml")).unwrap();
assert_eq!(transpiled, expected);
}
#[test]
fn parses_nullable_example() {
use pretty_assertions::assert_eq;
let path = Path::new("./examples/example_nullable_fields.yml").to_owned();
let parsed = OpenApi::from_path(&path).unwrap();
let scope = Scope {
use_nullable_for_merge_patch: true,
..Scope::default()
};
let transpiled = parsed.transpile(&scope).unwrap();
let expected =
OpenApi::from_path(Path::new("./examples/example_nullable_fields_output.yml"))
.unwrap();
assert_eq!(transpiled, expected);
}