use serde::{Deserialize, Serialize};
use crate::model::{
document::Document,
paths::{item::PathItem, operation::Operation},
reference::RefOr,
};
pub const UNCHECKED_SCHEMA_ANNOTATION: &str = "x-kynos-unchecked";
pub const OPAQUE_OPERATION_ANNOTATION: &str = "x-kynos-opaque";
pub const OPAQUE_ROUTES_ANNOTATION: &str = "x-kynos-opaque-routes";
pub const NOT_AUTHORITATIVE_ANNOTATION: &str = "x-kynos-document-not-authoritative";
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum OpaqueReason {
UntypedLayer,
UntypedRoute,
UntypedHandler,
ProtocolUpgrade,
StaticAssets,
#[serde(untagged)]
Unrecognized(String),
}
impl OpaqueReason {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::UntypedLayer => "untyped-layer",
Self::UntypedRoute => "untyped-route",
Self::UntypedHandler => "untyped-handler",
Self::ProtocolUpgrade => "protocol-upgrade",
Self::StaticAssets => "static-assets",
Self::Unrecognized(reason) => reason,
}
}
}
impl std::fmt::Display for OpaqueReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Opaque {
pub reasons: Vec<OpaqueReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl Opaque {
#[must_use]
pub fn new(reason: OpaqueReason) -> Self {
Self {
reasons: vec![reason],
note: None,
}
}
#[must_use]
pub fn with_reason(mut self, reason: OpaqueReason) -> Self {
self.add_reason(reason);
self
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn absorb(&mut self, other: &Self) {
for reason in &other.reasons {
self.add_reason(reason.clone());
}
if self.note.is_none() {
self.note.clone_from(&other.note);
}
}
fn add_reason(&mut self, reason: OpaqueReason) {
if !self.reasons.contains(&reason) {
self.reasons.push(reason);
}
}
#[must_use]
pub fn is_annotated(operation: &Operation) -> bool {
operation
.extensions
.get(OPAQUE_OPERATION_ANNOTATION)
.is_some()
}
pub fn of(operation: &Operation) -> Result<Option<Self>, MalformedAnnotation> {
let Some(value) = operation.extensions.get(OPAQUE_OPERATION_ANNOTATION) else {
return Ok(None);
};
serde_json::from_value(value.clone())
.map(Some)
.map_err(|error| MalformedAnnotation::new(OPAQUE_OPERATION_ANNOTATION, &error))
}
pub fn apply_to(&self, operation: &mut Operation) -> Result<(), MalformedAnnotation> {
let mut merged = Self::of(operation)?.unwrap_or_default();
merged.absorb(self);
let value = serde_json::to_value(&merged).expect("an opaque marker is always serializable");
operation
.extensions
.insert(OPAQUE_OPERATION_ANNOTATION, value);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OpaqueRoute {
pub pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub methods: Vec<String>,
pub reason: OpaqueReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl OpaqueRoute {
#[must_use]
pub fn new(pattern: impl Into<String>, reason: OpaqueReason) -> Self {
Self {
pattern: pattern.into(),
prefix: None,
methods: Vec::new(),
reason,
note: None,
}
}
#[must_use]
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self
}
#[must_use]
pub fn with_methods<I, S>(mut self, methods: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.methods = methods.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
#[must_use]
pub fn is_annotated(document: &Document) -> bool {
document.extensions.get(OPAQUE_ROUTES_ANNOTATION).is_some()
}
pub fn all(document: &Document) -> Result<Vec<Self>, MalformedAnnotation> {
let Some(value) = document.extensions.get(OPAQUE_ROUTES_ANNOTATION) else {
return Ok(Vec::new());
};
serde_json::from_value(value.clone())
.map_err(|error| MalformedAnnotation::new(OPAQUE_ROUTES_ANNOTATION, &error))
}
pub fn append_to(&self, document: &mut Document) -> Result<(), MalformedAnnotation> {
let mut routes = Self::all(document)?;
routes.push(self.clone());
let value = serde_json::to_value(&routes).expect("an opaque route is always serializable");
document.extensions.insert(OPAQUE_ROUTES_ANNOTATION, value);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("`{name}` is present but is not in the form Kynos emits: {detail}")]
pub struct MalformedAnnotation {
pub name: String,
pub detail: String,
}
impl MalformedAnnotation {
fn new(name: &str, error: &serde_json::Error) -> Self {
Self {
name: name.to_owned(),
detail: error.to_string(),
}
}
}
fn item_operations(item: &PathItem) -> Box<dyn Iterator<Item = &Operation> + '_> {
let declared = item.operations().map(|(_, operation)| operation);
#[cfg(feature = "openapi32")]
let declared = declared.chain(item.additional_operations.values().map(Box::as_ref));
Box::new(declared.flat_map(|operation| {
std::iter::once(operation).chain(
operation
.callbacks
.values()
.filter_map(RefOr::as_item)
.flat_map(|callback| callback.items.values())
.filter_map(RefOr::as_item)
.flat_map(item_operations),
)
}))
}
fn operations(document: &Document) -> impl Iterator<Item = &Operation> {
document
.paths
.items
.values()
.chain(document.webhooks.values())
.chain(document.components.path_items.values())
.flat_map(item_operations)
.chain(
document
.components
.callbacks
.values()
.filter_map(RefOr::as_item)
.flat_map(|callback| callback.items.values())
.filter_map(RefOr::as_item)
.flat_map(item_operations),
)
}
impl Document {
#[must_use]
pub fn is_authoritative(&self) -> bool {
let no_opaque_routes = OpaqueRoute::all(self).is_ok_and(|routes| routes.is_empty());
no_opaque_routes && !operations(self).any(Opaque::is_annotated)
}
pub fn restamp_authority(&mut self) {
if self.is_authoritative() {
self.extensions.remove(NOT_AUTHORITATIVE_ANNOTATION);
} else {
self.extensions.insert(NOT_AUTHORITATIVE_ANNOTATION, true);
}
}
}
#[cfg(test)]
mod tests;