use std::str::FromStr;
use derive_more::derive::{Display, Error};
use log::trace;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use super::Spec;
static RE_REF: Lazy<Regex> = Lazy::new(|| {
Regex::new("^(?P<source>[^#]*)#/components/(?P<type>[^/]+)/(?P<name>.+)$").unwrap()
});
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ObjectOrReference<T> {
Ref {
#[serde(rename = "$ref")]
ref_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
Object(T),
}
impl<T> ObjectOrReference<T>
where
T: FromRef,
{
pub fn resolve(&self, spec: &Spec) -> Result<T, RefError> {
match self {
Self::Object(component) => Ok(component.clone()),
Self::Ref { ref_path, .. } => T::from_ref(spec, ref_path),
}
}
}
#[derive(Debug, Clone, PartialEq, Display, Error)]
pub enum RefError {
#[display("Invalid type: {}", _0)]
UnknownType(#[error(not(source))] String),
#[display("Mismatched type: cannot reference a {} as a {}", _0, _1)]
MismatchedType(RefType, RefType),
#[display("Unresolvable path: {}", _0)]
Unresolvable(#[error(not(source))] String), }
#[derive(Debug, Clone, Copy, PartialEq, Display)]
pub enum RefType {
Schema,
Response,
Parameter,
Example,
RequestBody,
Header,
SecurityScheme,
Link,
Callback,
}
impl FromStr for RefType {
type Err = RefError;
fn from_str(typ: &str) -> Result<Self, Self::Err> {
Ok(match typ {
"schemas" => Self::Schema,
"responses" => Self::Response,
"parameters" => Self::Parameter,
"examples" => Self::Example,
"requestBodies" => Self::RequestBody,
"headers" => Self::Header,
"securitySchemes" => Self::SecurityScheme,
"links" => Self::Link,
"callbacks" => Self::Callback,
typ => return Err(RefError::UnknownType(typ.to_owned())),
})
}
}
#[derive(Debug, Clone)]
pub struct Ref {
pub source: String,
pub kind: RefType,
pub name: String,
}
impl FromStr for Ref {
type Err = RefError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
let parts = RE_REF
.captures(path)
.ok_or_else(|| RefError::Unresolvable(path.to_owned()))?;
trace!("creating Ref: {}/{}", &parts["type"], &parts["name"]);
Ok(Self {
source: parts["source"].to_owned(),
kind: parts["type"].parse()?,
name: parts["name"].to_owned(),
})
}
}
pub trait FromRef: Clone {
fn from_ref(spec: &Spec, path: &str) -> Result<Self, RefError>;
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use serde_json::json;
use super::{ObjectOrReference, Ref, RefError};
#[test]
fn ref_serialization_omits_empty_overrides() {
let reference = ObjectOrReference::<()>::Ref {
ref_path: "#/components/examples/RustMascot".to_owned(),
summary: None,
description: None,
};
let serialized = serde_json::to_value(reference).expect("serializing ref");
assert_eq!(
serialized,
json!({
"$ref": "#/components/examples/RustMascot",
})
);
}
#[test]
fn ref_serialization_includes_present_overrides() {
let reference = ObjectOrReference::<()>::Ref {
ref_path: "#/components/examples/RustMascot".to_owned(),
summary: Some("Rust mascot override".to_owned()),
description: Some("Let Ferris do the talking.".to_owned()),
};
let serialized = serde_json::to_value(reference).expect("serializing ref");
assert_eq!(
serialized,
json!({
"$ref": "#/components/examples/RustMascot",
"summary": "Rust mascot override",
"description": "Let Ferris do the talking.",
})
);
}
#[test]
fn invalid_ref_path_returns_error() {
let err = "/components/schemas/petdetails#pet_details_id"
.parse::<Ref>()
.expect_err("invalid $ref should not parse");
assert_matches!(
err,
RefError::Unresolvable(path) if path == "/components/schemas/petdetails#pet_details_id"
);
}
}