Skip to main content

kynos_openapi/model/
link.rs

1//! The Link Object.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::{
9    Map,
10    model::{extensions::Extensions, server::Server},
11};
12
13/// A design-time link from one response to another operation.
14///
15/// A link says "the value at this location in my response is the `id` parameter
16/// of *that* operation". It is the one construct in OpenAPI that expresses the
17/// relationship between operations, and no mainstream Rust framework emits it.
18///
19/// The target is a [`LinkTarget`], which is `operationRef` or `operationId`
20/// and never both or neither.
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(try_from = "RawLink", into = "RawLink")]
23pub struct Link {
24    target: LinkTarget,
25
26    /// Parameters to pass to the target operation.
27    ///
28    /// Each value is either a constant or a runtime expression such as
29    /// `$response.body#/id`.
30    pub parameters: Map<Value>,
31
32    /// A request body to pass to the target operation.
33    pub request_body: Option<Value>,
34
35    /// A description of the link. [CommonMark] syntax may be used.
36    ///
37    /// [CommonMark]: https://spec.commonmark.org/
38    pub description: Option<String>,
39
40    /// A server to use for the target operation.
41    pub server: Option<Server>,
42
43    /// Specification extensions.
44    pub extensions: Extensions,
45}
46
47/// How a link identifies the operation it points at.
48///
49/// An enum rather than two `Option` fields, for the reason
50/// [`SecurityScheme`](crate::model::security::SecurityScheme) is one: an
51/// unusable combination that cannot be spelled needs no rule to reject it. A
52/// link naming neither target points nowhere, and one naming both points at two
53/// operations without saying which wins.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum LinkTarget {
56    /// A URI reference to the target operation, written to `operationRef`.
57    ///
58    /// Named for what it holds rather than for its field: a variant called
59    /// `Ref` would read as the [`Ref`](crate::Ref) this crate already has, which
60    /// is a Reference Object and something else entirely.
61    Uri(String),
62
63    /// The [`operation_id`](crate::Operation::operation_id) of the target,
64    /// written to `operationId`.
65    Id(String),
66}
67
68impl Link {
69    /// Links to an operation by its `operationId`.
70    pub fn to_operation(operation_id: impl Into<String>) -> Self {
71        Self::targeting(LinkTarget::Id(operation_id.into()))
72    }
73
74    /// Links to an operation by URI reference.
75    pub fn to_operation_ref(operation_ref: impl Into<String>) -> Self {
76        Self::targeting(LinkTarget::Uri(operation_ref.into()))
77    }
78
79    fn targeting(target: LinkTarget) -> Self {
80        Self {
81            target,
82            parameters: Map::new(),
83            request_body: None,
84            description: None,
85            server: None,
86            extensions: Extensions::default(),
87        }
88    }
89
90    /// The operation this link points at.
91    #[must_use]
92    pub fn target(&self) -> &LinkTarget {
93        &self.target
94    }
95
96    /// The URI reference to the target, when the link points at one that way.
97    #[must_use]
98    pub fn operation_ref(&self) -> Option<&str> {
99        match &self.target {
100            LinkTarget::Uri(uri) => Some(uri),
101            LinkTarget::Id(_) => None,
102        }
103    }
104
105    /// The `operationId` of the target, when the link names one.
106    #[must_use]
107    pub fn operation_id(&self) -> Option<&str> {
108        match &self.target {
109            LinkTarget::Id(id) => Some(id),
110            LinkTarget::Uri(_) => None,
111        }
112    }
113
114    /// Binds a target parameter to a constant or runtime expression.
115    #[must_use]
116    pub fn with_parameter(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
117        self.parameters.insert(name.into(), value.into());
118        self
119    }
120
121    /// Sets the description.
122    #[must_use]
123    pub fn with_description(mut self, description: impl Into<String>) -> Self {
124        self.description = Some(description.into());
125        self
126    }
127}
128
129/// The wire shape: the target fields flat, as the specification writes them.
130#[derive(Serialize, Deserialize)]
131struct RawLink {
132    #[serde(
133        rename = "operationRef",
134        default,
135        skip_serializing_if = "Option::is_none"
136    )]
137    operation_ref: Option<String>,
138
139    #[serde(
140        rename = "operationId",
141        default,
142        skip_serializing_if = "Option::is_none"
143    )]
144    operation_id: Option<String>,
145
146    #[serde(default, skip_serializing_if = "Map::is_empty")]
147    parameters: Map<Value>,
148
149    #[serde(
150        rename = "requestBody",
151        default,
152        deserialize_with = "crate::model::nullable::some",
153        skip_serializing_if = "Option::is_none"
154    )]
155    request_body: Option<Value>,
156
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    description: Option<String>,
159
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    server: Option<Server>,
162
163    #[serde(flatten)]
164    extensions: Extensions,
165}
166
167/// A Link Object that does not identify exactly one target operation.
168#[derive(Debug)]
169enum LinkConflict {
170    /// Neither `operationRef` nor `operationId` was given.
171    Neither,
172
173    /// Both `operationRef` and `operationId` were given.
174    Both,
175}
176
177impl fmt::Display for LinkConflict {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::Neither => {
181                f.write_str("one of `operationRef` and `operationId` is required on a Link Object")
182            }
183            Self::Both => f.write_str(
184                "`operationRef` and `operationId` are mutually exclusive on a Link Object",
185            ),
186        }
187    }
188}
189
190impl TryFrom<RawLink> for Link {
191    type Error = LinkConflict;
192
193    fn try_from(raw: RawLink) -> Result<Self, Self::Error> {
194        let target = match (raw.operation_ref, raw.operation_id) {
195            (Some(_), Some(_)) => return Err(LinkConflict::Both),
196            (Some(uri), None) => LinkTarget::Uri(uri),
197            (None, Some(id)) => LinkTarget::Id(id),
198            (None, None) => return Err(LinkConflict::Neither),
199        };
200
201        Ok(Self {
202            target,
203            parameters: raw.parameters,
204            request_body: raw.request_body,
205            description: raw.description,
206            server: raw.server,
207            extensions: raw.extensions,
208        })
209    }
210}
211
212impl From<Link> for RawLink {
213    fn from(link: Link) -> Self {
214        let (operation_ref, operation_id) = match link.target {
215            LinkTarget::Uri(uri) => (Some(uri), None),
216            LinkTarget::Id(id) => (None, Some(id)),
217        };
218
219        Self {
220            operation_ref,
221            operation_id,
222            parameters: link.parameters,
223            request_body: link.request_body,
224            description: link.description,
225            server: link.server,
226            extensions: link.extensions,
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests;