use std::{collections::BTreeMap, fmt::Debug};
use anyhow::format_err;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use super::{
interface::InterfaceVariant, serde_helpers::deserialize_enum_helper, Scope,
Transpile,
};
pub trait ExpectedWhenParsing {
fn expected_when_parsing() -> &'static str;
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum RefOr<T>
where
T: Clone + Debug + Eq + PartialEq + Serialize,
{
Ref(Ref),
InterfaceRef(InterfaceRef),
Value(T),
}
impl<'de, T> Deserialize<'de> for RefOr<T>
where
T: Clone
+ Debug
+ DeserializeOwned
+ Eq
+ ExpectedWhenParsing
+ PartialEq
+ Serialize,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde_yaml::{Mapping, Value};
let yaml = Mapping::deserialize(deserializer)?;
let yaml_str = |s| Value::String(String::from(s));
if yaml.contains_key(&yaml_str("$ref")) {
Ok(RefOr::Ref(deserialize_enum_helper::<D, _>(
"$ref schema",
Value::Mapping(yaml),
)?))
} else if yaml.contains_key(&yaml_str("$interface")) {
Ok(RefOr::InterfaceRef(deserialize_enum_helper::<D, _>(
"$interface schema",
Value::Mapping(yaml),
)?))
} else {
Ok(RefOr::Value(deserialize_enum_helper::<D, _>(
&format!(
"$ref, $interface or {}",
<T as ExpectedWhenParsing>::expected_when_parsing(),
),
Value::Mapping(yaml),
)?))
}
}
}
impl<T> Transpile for RefOr<T>
where
T: Clone + Debug + Eq + PartialEq + Serialize + Transpile<Output = T>,
{
type Output = Self;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
match self {
RefOr::InterfaceRef(r) => Ok(RefOr::Ref(r.transpile(scope)?)),
RefOr::Ref(r) => Ok(RefOr::Ref(r.transpile(scope)?)),
RefOr::Value(val) => Ok(RefOr::Value(val.transpile(scope)?)),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Ref {
#[serde(rename = "$ref")]
target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Ref {
pub fn new<S: Into<String>>(target: S, description: Option<String>) -> Ref {
Ref {
target: target.into(),
description,
unknown_fields: Default::default(),
}
}
}
impl Transpile for Ref {
type Output = Self;
fn transpile(&self, _scope: &Scope) -> anyhow::Result<Self::Output> {
if !self.unknown_fields.is_empty() {
return Err(format_err!(
"`$ref:` no unknown sibling values allowed in {:?}",
self
));
}
Ok(self.clone())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InterfaceRef {
#[serde(rename = "$interface")]
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Transpile for InterfaceRef {
type Output = Ref;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
if !self.unknown_fields.is_empty() {
return Err(format_err!(
"`$interface:` no unknown sibling values allowed in {:?}",
self
));
}
let target = transpile_interface_ref_to_ref(&self.target, scope)?;
Ok(Ref::new(target, self.description.clone()))
}
}
pub fn split_interface_ref(interface_ref: &str) -> (&str, Option<&str>) {
let fragment_pos = interface_ref.find('#');
if let Some(fragment_pos) = fragment_pos {
(
&interface_ref[..fragment_pos],
Some(&interface_ref[fragment_pos..]),
)
} else {
(interface_ref, None)
}
}
pub fn transpile_interface_ref_to_ref(
interface_ref: &str,
scope: &Scope,
) -> Result<String, anyhow::Error> {
let (base, fragment) = split_interface_ref(interface_ref);
let fragment = fragment.unwrap_or("");
let variety = if fragment == "#SameAsInterface" {
if let Some(variant) = scope.variant {
variant
} else {
return Err(format_err!(
"cannot use #SameAsInterface outside of a `components.interfaces` declaration"
));
}
} else {
fragment.parse::<InterfaceVariant>()?
};
let target = format!(
"#/components/schemas/{}{}",
base,
variety.to_schema_suffix_str()
);
Ok(target)
}
#[test]
fn transpile_interface_with_description() {
let interface_ref = InterfaceRef {
target: "Widget".to_string(),
description: Some("hello".to_string()),
unknown_fields: Default::default(),
};
let actual_ref = interface_ref.transpile(&Scope::default()).unwrap();
let expected_ref = Ref {
target: "#/components/schemas/Widget".to_string(),
description: Some("hello".to_string()),
unknown_fields: Default::default(),
};
assert_eq!(actual_ref, expected_ref);
}