Skip to main content

ploidy_core/ir/views/
operation.rs

1//! Operations: per-path methods with parameter, request, and response schemas.
2//!
3//! In OpenAPI, each path item defines operations for HTTP methods like
4//! `GET` and `POST`. An operation has path and query parameters, an
5//! optional request body, and an optional response body:
6//!
7//! ```yaml
8//! paths:
9//!   /pets/{pet_id}:
10//!     post:
11//!       operationId: updatePet
12//!       parameters:
13//!         - name: pet_id
14//!           in: path
15//!           required: true
16//!           schema:
17//!             type: string
18//!         - name: expand
19//!           in: query
20//!           schema:
21//!             type: boolean
22//!       requestBody:
23//!         content:
24//!           application/json:
25//!             schema:
26//!               $ref: '#/components/schemas/PetUpdate'
27//!       responses:
28//!         '200':
29//!           content:
30//!             application/json:
31//!               schema:
32//!                 $ref: '#/components/schemas/Pet'
33//! ```
34//!
35//! Ploidy represents this as an [`OperationView`] with:
36//!
37//! * An [ID], an [HTTP method], and a [path template] with
38//!   segments and path parameters.
39//! * [Query parameters], each with a name, type, and
40//!   optional serialization style.
41//! * An optional [request] and [response] body, each wrapping
42//!   a [`TypeView`] of the body schema.
43//! * An optional [resource name] from the `x-resource-name` extension,
44//!   used to group operations by resource.
45//!
46//! Unlike types, operations are not nodes in Ploidy's dependency graph,
47//! but they implement [`View`] for traversal.
48//!
49//! [ID]: OperationView::id
50//! [HTTP method]: OperationView::method
51//! [path template]: OperationView::path
52//! [Query parameters]: OperationView::query
53//! [request]: OperationView::request
54//! [response]: OperationView::response
55//! [resource name]: OperationView::resource
56
57use std::{
58    collections::VecDeque,
59    fmt::{Display, Formatter, Result as FmtResult},
60    marker::PhantomData,
61};
62
63use petgraph::{
64    graph::NodeIndex,
65    visit::{Bfs, EdgeFiltered, EdgeRef, Visitable},
66};
67
68use crate::{
69    ir::{
70        graph::CookedGraph,
71        types::{
72            GraphOperation, GraphParameter, GraphParameterInfo, GraphRequest, GraphResponse,
73            GraphType, OperationId, ParameterStyle,
74        },
75    },
76    parse::{
77        Method,
78        path::{PathQueryParameter, PathRuns, PathSegment},
79    },
80};
81
82use super::{HasResource, View, inline::InlineTypeView, ir::TypeView};
83
84/// A graph-aware view of an [operation][GraphOperation].
85#[derive(Debug)]
86pub struct OperationView<'graph, 'a> {
87    cooked: &'graph CookedGraph<'a>,
88    op: &'graph GraphOperation<'a>,
89}
90
91impl<'graph, 'a> OperationView<'graph, 'a> {
92    #[inline]
93    pub(in crate::ir) fn new(
94        cooked: &'graph CookedGraph<'a>,
95        op: &'graph GraphOperation<'a>,
96    ) -> Self {
97        Self { cooked, op }
98    }
99
100    /// Returns the `operationId`.
101    #[inline]
102    pub fn id(&self) -> &'a OperationId {
103        OperationId::new(self.op.id)
104    }
105
106    /// Returns the HTTP method.
107    #[inline]
108    pub fn method(&self) -> Method {
109        self.op.method
110    }
111
112    /// Returns a view of this operation's path template.
113    #[inline]
114    pub fn path(&self) -> OperationViewPath<'_, 'graph, 'a> {
115        OperationViewPath(self)
116    }
117
118    /// Returns the description, if present in the spec.
119    #[inline]
120    pub fn description(&self) -> Option<&'a str> {
121        self.op.description
122    }
123
124    /// Returns an iterator over this operation's query parameters.
125    #[inline]
126    pub fn query(&self) -> impl Iterator<Item = ParameterView<'_, 'graph, 'a, QueryParameter>> {
127        self.op.params.iter().filter_map(|param| match param {
128            GraphParameter::Query(info) => Some(ParameterView::new(self, info)),
129            _ => None,
130        })
131    }
132
133    /// Returns a view of the request body, if present.
134    #[inline]
135    pub fn request(&self) -> Option<RequestView<'graph, 'a>> {
136        self.op.request.as_ref().map(|ty| match ty {
137            GraphRequest::Json(index) => RequestView::Json(TypeView::new(self.cooked, *index)),
138            GraphRequest::Multipart => RequestView::Multipart,
139        })
140    }
141
142    /// Returns a view of the response body, if present.
143    #[inline]
144    pub fn response(&self) -> Option<ResponseView<'graph, 'a>> {
145        self.op.response.as_ref().map(|ty| match ty {
146            GraphResponse::Json(index) => ResponseView::Json(TypeView::new(self.cooked, *index)),
147        })
148    }
149}
150
151impl<'a> HasResource<'a> for OperationView<'_, 'a> {
152    /// Returns the resource name that this operation declares
153    /// in its `x-resource-name` extension field.
154    #[inline]
155    fn resource(&self) -> Option<&'a str> {
156        self.op.resource
157    }
158}
159
160impl<'graph, 'a> View<'graph, 'a> for OperationView<'graph, 'a> {
161    /// Returns an iterator over all the inline types that are
162    /// contained within this operation's referenced types.
163    #[inline]
164    fn inlines(&self) -> impl Iterator<Item = InlineTypeView<'graph, 'a>> + use<'graph, 'a> {
165        let cooked = self.cooked;
166        // Follow edges to inline schemas, skipping shadow edges.
167        // See `GraphEdge::shadow()` for an explanation.
168        let filtered = EdgeFiltered::from_fn(&cooked.graph, |e| {
169            !e.weight().shadow() && matches!(cooked.graph[e.target()], GraphType::Inline(_))
170        });
171        let mut bfs = {
172            let stack: VecDeque<_> = self
173                .op
174                .types()
175                .copied()
176                .filter(|&index| {
177                    // Exclude operation types that aren't inline schemas;
178                    // those types, and their inlines, are already emitted
179                    // as named schema types.
180                    matches!(cooked.graph[index], GraphType::Inline(_))
181                })
182                .collect();
183            let mut discovered = self.cooked.graph.visit_map();
184            discovered.extend(stack.iter().copied().map(NodeIndex::index));
185            Bfs { stack, discovered }
186        };
187        // Unlike `View::inlines()`, we include the starting nodes:
188        // the operation contains types; it's not a type itself.
189        std::iter::from_fn(move || bfs.next(&filtered)).filter_map(|index| {
190            match cooked.graph[index] {
191                GraphType::Inline(ty) => Some(InlineTypeView::new(cooked, index, ty)),
192                _ => None,
193            }
194        })
195    }
196
197    /// Returns an empty iterator. Operations aren't "used by" other operations;
198    /// they use types.
199    #[inline]
200    fn used_by(&self) -> impl Iterator<Item = OperationView<'graph, 'a>> + use<'graph, 'a> {
201        std::iter::empty()
202    }
203
204    #[inline]
205    fn dependencies(&self) -> impl Iterator<Item = TypeView<'graph, 'a>> + use<'graph, 'a> {
206        let cooked = self.cooked;
207        cooked.metadata.uses[self.op]
208            .ones()
209            .map(NodeIndex::new)
210            .map(|index| TypeView::new(cooked, index))
211    }
212
213    /// Returns an empty iterator. Operations don't have dependents.
214    #[inline]
215    fn dependents(&self) -> impl Iterator<Item = TypeView<'graph, 'a>> + use<'graph, 'a> {
216        std::iter::empty()
217    }
218
219    #[inline]
220    fn hashable(&self) -> bool {
221        false
222    }
223
224    #[inline]
225    fn defaultable(&self) -> bool {
226        false
227    }
228}
229
230/// A graph-aware view of operation's path template and parameters.
231#[derive(Clone, Copy, Debug)]
232pub struct OperationViewPath<'view, 'graph, 'a>(&'view OperationView<'graph, 'a>);
233
234impl<'view, 'graph, 'a> OperationViewPath<'view, 'graph, 'a> {
235    /// Returns an iterator over this path's segments.
236    #[inline]
237    pub fn segments(self) -> std::slice::Iter<'view, PathSegment<'a>> {
238        self.0.op.path.segments.iter()
239    }
240
241    /// Returns this path's segments coalesced into runs.
242    #[inline]
243    pub fn runs(self) -> PathRuns<'view, 'a> {
244        self.0.op.path.runs()
245    }
246
247    /// Returns an iterator over any literal query parameters
248    /// after the path template.
249    #[inline]
250    pub fn query(self) -> std::slice::Iter<'view, PathQueryParameter<'a>> {
251        self.0.op.path.query.iter()
252    }
253
254    /// Returns an iterator over this operation's path parameters.
255    #[inline]
256    pub fn params(self) -> impl Iterator<Item = ParameterView<'view, 'graph, 'a, PathParameter>> {
257        self.0.op.params.iter().filter_map(|param| match param {
258            GraphParameter::Path(info) => Some(ParameterView::new(self.0, info)),
259            _ => None,
260        })
261    }
262}
263
264impl Display for OperationViewPath<'_, '_, '_> {
265    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
266        write!(f, "{}", self.0.op.path)
267    }
268}
269
270/// A graph-aware view of an operation parameter.
271#[derive(Debug)]
272pub struct ParameterView<'view, 'graph, 'a, T> {
273    op: &'view OperationView<'graph, 'a>,
274    info: &'a GraphParameterInfo<'a>,
275    phantom: PhantomData<T>,
276}
277
278impl<'view, 'graph, 'a, T> ParameterView<'view, 'graph, 'a, T> {
279    #[inline]
280    pub(in crate::ir) fn new(
281        op: &'view OperationView<'graph, 'a>,
282        info: &'a GraphParameterInfo<'a>,
283    ) -> Self {
284        Self {
285            op,
286            info,
287            phantom: PhantomData,
288        }
289    }
290
291    /// Returns the parameter name.
292    #[inline]
293    pub fn name(&self) -> &'a str {
294        self.info.name
295    }
296
297    /// Returns a view of the parameter's type.
298    #[inline]
299    pub fn ty(&self) -> TypeView<'graph, 'a> {
300        TypeView::new(self.op.cooked, self.info.ty)
301    }
302
303    /// Returns `true` if this parameter is required.
304    #[inline]
305    pub fn required(&self) -> bool {
306        self.info.required
307    }
308
309    /// Returns the serialization style, if specified.
310    #[inline]
311    pub fn style(&self) -> Option<ParameterStyle> {
312        self.info.style
313    }
314}
315
316impl<'view, 'graph, 'a, T> View<'graph, 'a> for ParameterView<'view, 'graph, 'a, T> {
317    fn inlines(
318        &self,
319    ) -> impl Iterator<Item = InlineTypeView<'graph, 'a>> + use<'view, 'graph, 'a, T> {
320        let cooked = self.op.cooked;
321        let start = self.info.ty;
322        // Follow edges to inline schemas, skipping shadow edges.
323        // See `GraphEdge::shadow()` for an explanation.
324        let filtered = EdgeFiltered::from_fn(&cooked.graph, |e| {
325            !e.weight().shadow() && matches!(cooked.graph[e.target()], GraphType::Inline(_))
326        });
327        let mut bfs = {
328            // Exclude parameter types that aren't inline schemas;
329            // those types, and their inlines, are already emitted as
330            // named schema types.
331            let stack = match cooked.graph[start] {
332                GraphType::Inline(_) => std::iter::once(start).collect(),
333                _ => VecDeque::new(),
334            };
335            let mut discovered = cooked.graph.visit_map();
336            discovered.extend(stack.iter().copied().map(NodeIndex::index));
337            Bfs { stack, discovered }
338        };
339        // Unlike `View::inlines()`, we include the starting node:
340        // the parameter references a type; it's not a type itself.
341        std::iter::from_fn(move || bfs.next(&filtered)).filter_map(|index| {
342            match cooked.graph[index] {
343                GraphType::Inline(ty) => Some(InlineTypeView::new(cooked, index, ty)),
344                _ => None,
345            }
346        })
347    }
348
349    fn used_by(
350        &self,
351    ) -> impl Iterator<Item = OperationView<'graph, 'a>> + use<'view, 'graph, 'a, T> {
352        std::iter::once(OperationView::new(self.op.cooked, self.op.op))
353    }
354
355    fn dependencies(
356        &self,
357    ) -> impl Iterator<Item = TypeView<'graph, 'a>> + use<'view, 'graph, 'a, T> {
358        let cooked = self.op.cooked;
359        cooked
360            .metadata
361            .closure
362            .dependencies_of(self.info.ty)
363            .map(|index| TypeView::new(cooked, index))
364    }
365
366    /// Returns an empty iterator; other types don't depend on parameters.
367    fn dependents(&self) -> impl Iterator<Item = TypeView<'graph, 'a>> + use<'view, 'graph, 'a, T> {
368        std::iter::empty()
369    }
370
371    #[inline]
372    fn hashable(&self) -> bool {
373        self.op.cooked.metadata.hashable[self.info.ty.index()]
374    }
375
376    #[inline]
377    fn defaultable(&self) -> bool {
378        self.op.cooked.metadata.defaultable[self.info.ty.index()]
379    }
380}
381
382/// A marker type for a path parameter.
383#[derive(Clone, Copy, Debug)]
384pub enum PathParameter {}
385
386/// A marker type for a query parameter.
387#[derive(Clone, Copy, Debug)]
388pub enum QueryParameter {}
389
390/// A graph-aware view of an operation's request body.
391#[derive(Debug)]
392pub enum RequestView<'graph, 'a> {
393    Json(TypeView<'graph, 'a>),
394    Multipart,
395}
396
397/// A graph-aware view of an operation's response body.
398#[derive(Debug)]
399pub enum ResponseView<'graph, 'a> {
400    Json(TypeView<'graph, 'a>),
401}