Skip to main content

ploidy_core/ir/views/
container.rs

1//! Container types: arrays, maps, and optionals.
2//!
3//! In OpenAPI, `type: array` with `items` defines a list,
4//! and `type: object` without `properties` and with
5//! `additionalProperties` defines a map. Schemas with
6//! `nullable: true` (OpenAPI 3.0), `type: [T, "null"]`
7//! (OpenAPI 3.1), or `oneOf` with a `null` branch all
8//! become optionals:
9//!
10//! ```yaml
11//! components:
12//!   schemas:
13//!     Tags:
14//!       type: array
15//!       items:
16//!         type: string
17//!     Metadata:
18//!       type: object
19//!       additionalProperties:
20//!         type: string
21//!     NullableName:
22//!       type: [string, null]
23//! ```
24//!
25//! Ploidy represents all three as [`ContainerView`] variants—
26//! [`Array`][array], [`Map`][map], and [`Optional`][opt]—
27//! each wrapping an [`InnerView`] that provides access to
28//! the contained type.
29//!
30//! [array]: ContainerView::Array
31//! [map]: ContainerView::Map
32//! [opt]: ContainerView::Optional
33
34use itertools::Itertools;
35use petgraph::{Direction, graph::NodeIndex, visit::EdgeRef};
36
37use crate::ir::{
38    graph::{CookedGraph, GraphEdge},
39    types::{GraphContainer, GraphInlineType, GraphSchemaType, GraphType},
40};
41
42use super::{TypeView, ViewNode};
43
44/// A graph-aware view of a [container type][GraphContainer].
45#[derive(Debug)]
46pub enum ContainerView<'graph, 'a> {
47    Array(InnerView<'graph, 'a>),
48    Map(InnerView<'graph, 'a>),
49    Optional(InnerView<'graph, 'a>),
50}
51
52impl<'graph, 'a> ContainerView<'graph, 'a> {
53    /// Returns a type view of this container type.
54    #[inline]
55    pub fn ty(&self) -> TypeView<'graph, 'a> {
56        TypeView::new(self.cooked(), self.index())
57    }
58}
59
60impl<'graph, 'a> ViewNode<'graph, 'a> for ContainerView<'graph, 'a> {
61    #[inline]
62    fn cooked(&self) -> &'graph CookedGraph<'a> {
63        let (Self::Array(c) | Self::Map(c) | Self::Optional(c)) = self;
64        c.cooked
65    }
66
67    #[inline]
68    fn index(&self) -> NodeIndex<usize> {
69        let (Self::Array(c) | Self::Map(c) | Self::Optional(c)) = self;
70        c.container
71    }
72}
73
74/// A graph-aware view of the inner type of a [container][ContainerView].
75#[derive(Debug)]
76pub struct InnerView<'graph, 'a> {
77    cooked: &'graph CookedGraph<'a>,
78    container: NodeIndex<usize>,
79    inner: NodeIndex<usize>,
80}
81
82impl<'graph, 'a> InnerView<'graph, 'a> {
83    /// Returns a view of the contained type.
84    #[inline]
85    pub fn ty(&self) -> TypeView<'graph, 'a> {
86        TypeView::new(self.cooked, self.inner)
87    }
88
89    /// Returns a human-readable description of the contained type, if present.
90    #[inline]
91    pub fn description(&self) -> Option<&'a str> {
92        match self.cooked.graph[self.container] {
93            GraphType::Schema(GraphSchemaType::Container(
94                _,
95                GraphContainer::Array { description }
96                | GraphContainer::Map { description }
97                | GraphContainer::Optional { description },
98            ))
99            | GraphType::Inline(GraphInlineType::Container(
100                _,
101                GraphContainer::Array { description }
102                | GraphContainer::Map { description }
103                | GraphContainer::Optional { description },
104            )) => description,
105            _ => None,
106        }
107    }
108}
109
110impl<'graph, 'a> ContainerView<'graph, 'a> {
111    #[inline]
112    pub(in crate::ir) fn new(
113        cooked: &'graph CookedGraph<'a>,
114        index: NodeIndex<usize>,
115        container: GraphContainer<'a>,
116    ) -> Self {
117        // Container nodes always have a `Contains` edge
118        // to their inner type.
119        let inner = cooked
120            .graph
121            .edges_directed(index, Direction::Outgoing)
122            .filter(|e| matches!(e.weight(), GraphEdge::Contains))
123            .map(|e| e.target())
124            .exactly_one()
125            .unwrap();
126        let inner = InnerView {
127            cooked,
128            container: index,
129            inner,
130        };
131        match container {
132            GraphContainer::Array { .. } => Self::Array(inner),
133            GraphContainer::Map { .. } => Self::Map(inner),
134            GraphContainer::Optional { .. } => Self::Optional(inner),
135        }
136    }
137}