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