Skip to main content

ploidy_codegen_rust/
graph.rs

1use std::{collections::BTreeSet, fmt::Write, ops::Deref};
2
3use ploidy_core::{
4    ir::{
5        ContainerView, CookedGraph, EnumVariant, EnumView, HasResource, HasTypeId,
6        InlineTypePathRoot, InlineTypePathSegment, InlineTypePathView, InlineTypeView, OperationId,
7        OperationUsage, SchemaTypeView, StructFieldName, StructView, TaggedView, TypeId, TypeView,
8        UntaggedView, View,
9    },
10    parse::ParameterLocation,
11};
12use rustc_hash::FxHashMap;
13
14use super::{
15    config::{CodegenConfig, DateTimeFormat},
16    naming::{CodegenIdentUsage, ResourceGroup, UniqueIdent, UniqueIdents},
17};
18
19/// A [`CookedGraph`] decorated with Rust-specific information.
20#[derive(Debug)]
21pub struct CodegenGraph<'a> {
22    cooked: CookedGraph<'a>,
23    idents: IdentMap<'a>,
24    date_time_format: DateTimeFormat,
25}
26
27impl<'a> CodegenGraph<'a> {
28    /// Wraps a type graph with the default configuration.
29    #[inline]
30    pub fn new(cooked: CookedGraph<'a>) -> Self {
31        Self::with_config(cooked, &CodegenConfig::default())
32    }
33
34    /// Wraps a type graph with the given configuration.
35    #[inline]
36    pub fn with_config(cooked: CookedGraph<'a>, config: &CodegenConfig) -> Self {
37        let idents = ident_map(&cooked);
38        Self {
39            cooked,
40            idents,
41            date_time_format: config.date_time_format,
42        }
43    }
44
45    /// Returns the unique Rust identifier for a schema, operation, parameter,
46    /// field, or variant.
47    #[inline]
48    pub fn ident(&self, key: impl Into<IdentMapping<'a>>) -> &'a UniqueIdent {
49        use {IdentMapKey as Key, IdentMapping::*};
50        match key.into() {
51            Operation(op) => self.idents[&Key::Operation(op)],
52            Path(op, name) => self.idents[&Key::Parameter(op, ParameterLocation::Path, name)],
53            Query(op, name) => self.idents[&Key::Parameter(op, ParameterLocation::Query, name)],
54            Type(id) => match self.cooked.view(id) {
55                TypeView::Schema(s) => self.idents[&Key::Schema(s.name())],
56                TypeView::Inline(i) => self.idents[&Key::Inline(i.id())],
57            },
58            StructField(id, name) => self.idents[&Key::StructField(id, name)],
59            EnumVariant(id, name) => self.idents[&Key::EnumVariant(id, name)],
60            TaggedVariant(id, name) => self.idents[&Key::TaggedVariant(id, name)],
61            UntaggedVariant(id, index) => self.idents[&Key::UntaggedVariant(id, index)],
62            Resource(name) => self.idents[&IdentMapKey::Resource(name)],
63        }
64    }
65
66    /// Returns the resource that contains the given view.
67    #[inline]
68    pub fn resource_for(&self, view: &impl HasResource<'a>) -> ResourceGroup<'a> {
69        view.resource()
70            .map(|name| ResourceGroup::Named(self.idents[&IdentMapKey::Resource(name)]))
71            .unwrap_or_default()
72    }
73
74    /// Returns the format to use for `date-time` types.
75    #[inline]
76    pub fn date_time_format(&self) -> DateTimeFormat {
77        self.date_time_format
78    }
79}
80
81impl<'a> Deref for CodegenGraph<'a> {
82    type Target = CookedGraph<'a>;
83
84    #[inline]
85    fn deref(&self) -> &Self::Target {
86        &self.cooked
87    }
88}
89
90/// An item with a uniquified Rust identifier in a [`CodegenGraph`].
91pub enum IdentMapping<'a> {
92    /// A schema or inline type.
93    Type(TypeId),
94
95    /// An operation method.
96    Operation(&'a OperationId),
97
98    /// A path parameter for an operation.
99    Path(&'a OperationId, &'a str),
100
101    /// A query parameter for an operation.
102    Query(&'a OperationId, &'a str),
103
104    /// A struct field.
105    StructField(TypeId, StructFieldName<'a>),
106
107    /// A string enum variant.
108    EnumVariant(TypeId, &'a str),
109
110    /// A tagged union variant.
111    TaggedVariant(TypeId, &'a str),
112
113    /// An untagged union variant.
114    UntaggedVariant(TypeId, usize),
115
116    /// ...
117    Resource(&'a str),
118}
119
120impl<'a> From<&'a OperationId> for IdentMapping<'a> {
121    #[inline]
122    fn from(id: &'a OperationId) -> Self {
123        Self::Operation(id)
124    }
125}
126
127impl<'a> From<TypeId> for IdentMapping<'a> {
128    #[inline]
129    fn from(id: TypeId) -> Self {
130        Self::Type(id)
131    }
132}
133
134/// Builds the identifier table for every name that Rust code generation emits.
135///
136/// Names are assigned in dependency order. Schema, operation, parameter, field,
137/// and variant identifiers are uniquified first; inline type names are composed
138/// from those earlier identifiers.
139fn ident_map<'a>(cooked: &CookedGraph<'a>) -> IdentMap<'a> {
140    let mut idents = FxHashMap::default();
141    idents.extend({
142        let mut scope = UniqueIdents::new(cooked.arena());
143        cooked
144            .schemas()
145            .map(move |ty| (IdentMapKey::Schema(ty.name()), scope.ident(ty.name())))
146    });
147    idents.extend({
148        let mut scope = UniqueIdents::new(cooked.arena());
149        cooked
150            .operations()
151            .map(move |op| (IdentMapKey::Operation(op.id()), scope.ident(op.id())))
152    });
153    idents.extend({
154        let resources: BTreeSet<_> = cooked
155            .operations()
156            .filter_map(|op| op.resource())
157            .chain(cooked.schemas().filter_map(|ty| ty.resource()))
158            .collect();
159        // Resources become feature names; `default` is a special feature name.
160        let mut scope = UniqueIdents::with_reserved(cooked.arena(), &["default"]);
161        resources
162            .into_iter()
163            .map(move |name| (IdentMapKey::Resource(name), scope.ident(name)))
164    });
165    for op in cooked.operations() {
166        {
167            // Path parameters become arguments, so we need to reserve
168            // local variable and argument names that we use in the
169            // generated operation method body.
170            let mut scope = UniqueIdents::with_reserved(
171                cooked.arena(),
172                &["query", "request", "form", "url", "response"],
173            );
174            for param in op.path().params() {
175                let ident = scope.ident(param.name());
176                idents.insert(
177                    IdentMapKey::Parameter(op.id(), ParameterLocation::Path, param.name()),
178                    ident,
179                );
180            }
181        }
182        {
183            // Query parameters become regular struct fields.
184            let mut scope = UniqueIdents::new(cooked.arena());
185            for param in op.query() {
186                let ident = scope.ident(param.name());
187                idents.insert(
188                    IdentMapKey::Parameter(op.id(), ParameterLocation::Query, param.name()),
189                    ident,
190                );
191            }
192        }
193    }
194
195    {
196        let domains = cooked
197            .schemas()
198            .filter_map(MemberIdentDomain::from_schema_type)
199            .chain(
200                cooked
201                    .schemas()
202                    .flat_map(|s| s.inlines())
203                    .chain(cooked.operations().flat_map(|op| op.inlines()))
204                    .filter_map(MemberIdentDomain::from_inline_type),
205            );
206        for domain in domains {
207            match domain {
208                // Own, inherited, and synthesized struct fields.
209                MemberIdentDomain::Struct(id, view) => {
210                    let mut scope = UniqueIdents::new(cooked.arena());
211                    for field in view.fields() {
212                        let ident = match field.name() {
213                            StructFieldName::Name(n) => scope.ident(n),
214                            StructFieldName::Hint(hint) => scope.field_name_hint(hint),
215                        };
216                        idents.insert(IdentMapKey::StructField(id, field.name()), ident);
217                    }
218                }
219                // Common fields inherited by all untagged variants. The struct arm
220                // uniquifies them for fields; this arm uniquifies them for
221                // naming inline types.
222                MemberIdentDomain::Untagged(id, view) => {
223                    let mut scope = UniqueIdents::new(cooked.arena());
224                    for (index, variant) in view.variants().enumerate() {
225                        let ident = match variant.ty() {
226                            Some(variant) => scope.variant_name_hint(variant.hint),
227                            None => scope.ident("None"),
228                        };
229                        idents.insert(IdentMapKey::UntaggedVariant(id, index), ident);
230                    }
231                    let mut scope = UniqueIdents::new(cooked.arena());
232                    for field in view.fields() {
233                        let ident = match field.name() {
234                            StructFieldName::Name(n) => scope.ident(n),
235                            StructFieldName::Hint(hint) => scope.field_name_hint(hint),
236                        };
237                        idents.insert(IdentMapKey::StructField(id, field.name()), ident);
238                    }
239                }
240                // String enum variants.
241                MemberIdentDomain::Enum(id, view) => {
242                    let mut scope = UniqueIdents::new(cooked.arena());
243                    for variant in view.variants() {
244                        if let EnumVariant::String(name) = variant {
245                            let ident = scope.ident(name);
246                            idents.insert(IdentMapKey::EnumVariant(id, name), ident);
247                        }
248                    }
249                }
250                // Tagged variant names and common fields form different scopes:
251                // variant names must be unique within the generated enum;
252                // common fields are for naming inline types.
253                MemberIdentDomain::Tagged(id, view) => {
254                    let mut scope = UniqueIdents::new(cooked.arena());
255                    for variant in view.variants() {
256                        let ident = scope.ident(variant.name());
257                        idents.insert(IdentMapKey::TaggedVariant(id, variant.name()), ident);
258                    }
259                    let mut scope = UniqueIdents::new(cooked.arena());
260                    for field in view.fields() {
261                        let ident = match field.name() {
262                            StructFieldName::Name(n) => scope.ident(n),
263                            StructFieldName::Hint(hint) => scope.field_name_hint(hint),
264                        };
265                        idents.insert(IdentMapKey::StructField(id, field.name()), ident);
266                    }
267                }
268            }
269        }
270    }
271
272    // Inline type names depend on uniquified path segments, so build them after
273    // all schemas, operations, parameters, fields, and variants have names.
274    {
275        let inlines = cooked
276            .schemas()
277            .flat_map(|schema| schema.inlines())
278            .chain(cooked.operations().flat_map(|op| op.inlines()))
279            .filter(|ty| {
280                // Optional types are invisible for naming.
281                !matches!(ty, InlineTypeView::Container(_, ContainerView::Optional(_)))
282            });
283
284        let mut scopes = FxHashMap::default();
285        for inline in inlines {
286            let path = inline.path();
287            let domain = match path.root() {
288                InlineTypePathRoot::Schema(id) => InlineTypeIdentDomain::Schema(id),
289                InlineTypePathRoot::Operation { resource, .. } => InlineTypeIdentDomain::Resource(
290                    resource
291                        .map(|name| ResourceGroup::Named(idents[&IdentMapKey::Resource(name)]))
292                        .unwrap_or_default(),
293                ),
294            };
295            let name = inline_type_candidate_name(&idents, &path);
296            let scope = scopes
297                .entry(domain)
298                .or_insert_with(|| UniqueIdents::new(cooked.arena()));
299            idents.insert(IdentMapKey::Inline(inline.id()), scope.ident(&name));
300        }
301    }
302    idents
303}
304
305type IdentMap<'a> = FxHashMap<IdentMapKey<'a>, &'a UniqueIdent>;
306
307#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
308enum IdentMapKey<'a> {
309    Schema(&'a str),
310    Inline(TypeId),
311    Operation(&'a OperationId),
312    Parameter(&'a OperationId, ParameterLocation, &'a str),
313    Resource(&'a str),
314    StructField(TypeId, StructFieldName<'a>),
315    EnumVariant(TypeId, &'a str),
316    TaggedVariant(TypeId, &'a str),
317    UntaggedVariant(TypeId, usize),
318}
319
320/// A uniqueness domain for inline type identifiers.
321#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
322enum InlineTypeIdentDomain<'a> {
323    Schema(TypeId),
324    Resource(ResourceGroup<'a>),
325}
326
327/// A uniqueness domain for field and variant identifiers.
328enum MemberIdentDomain<'graph, 'a> {
329    Struct(TypeId, StructView<'graph, 'a>),
330    Enum(TypeId, EnumView<'graph, 'a>),
331    Tagged(TypeId, TaggedView<'graph, 'a>),
332    Untagged(TypeId, UntaggedView<'graph, 'a>),
333}
334
335impl<'graph, 'a> MemberIdentDomain<'graph, 'a> {
336    fn from_schema_type(schema: SchemaTypeView<'graph, 'a>) -> Option<Self> {
337        let id = schema.id();
338        Some(match schema {
339            SchemaTypeView::Struct(_, v) => Self::Struct(id, v),
340            SchemaTypeView::Enum(_, v) => Self::Enum(id, v),
341            SchemaTypeView::Tagged(_, v) => Self::Tagged(id, v),
342            SchemaTypeView::Untagged(_, v) => Self::Untagged(id, v),
343            _ => return None,
344        })
345    }
346
347    fn from_inline_type(inline: InlineTypeView<'graph, 'a>) -> Option<Self> {
348        let id = inline.id();
349        Some(match inline {
350            InlineTypeView::Struct(_, v) => Self::Struct(id, v),
351            InlineTypeView::Enum(_, v) => Self::Enum(id, v),
352            InlineTypeView::Tagged(_, v) => Self::Tagged(id, v),
353            InlineTypeView::Untagged(_, v) => Self::Untagged(id, v),
354            _ => return None,
355        })
356    }
357}
358
359fn inline_type_candidate_name<'a>(
360    idents: &IdentMap<'a>,
361    path: &InlineTypePathView<'_, 'a>,
362) -> String {
363    let mut name = String::new();
364
365    match path.root() {
366        InlineTypePathRoot::Schema(_) => {}
367        InlineTypePathRoot::Operation { id, usage, .. } => {
368            let ident = idents[&IdentMapKey::Operation(id)];
369            write!(name, "{}", CodegenIdentUsage::Type(ident).display()).unwrap();
370
371            match usage {
372                OperationUsage::Path(param) => {
373                    let ident = idents[&IdentMapKey::Parameter(id, ParameterLocation::Path, param)];
374                    write!(name, "Path{}", CodegenIdentUsage::Type(ident).display()).unwrap();
375                }
376                OperationUsage::Query(param) => {
377                    let ident =
378                        idents[&IdentMapKey::Parameter(id, ParameterLocation::Query, param)];
379                    write!(name, "Query{}", CodegenIdentUsage::Type(ident).display()).unwrap();
380                }
381                OperationUsage::Request => name.push_str("Request"),
382                OperationUsage::Response => name.push_str("Response"),
383            }
384        }
385    }
386
387    for segment in path.segments() {
388        match segment {
389            InlineTypePathSegment::Field(parent, field_name) => {
390                let ident = idents[&IdentMapKey::StructField(parent, field_name)];
391                write!(name, "{}", CodegenIdentUsage::Type(ident).display()).unwrap();
392            }
393            InlineTypePathSegment::TaggedVariant(parent, variant_name) => {
394                let ident = idents[&IdentMapKey::TaggedVariant(parent, variant_name)];
395                write!(name, "{}", CodegenIdentUsage::Variant(ident).display()).unwrap();
396            }
397            InlineTypePathSegment::UntaggedVariant(index) => {
398                write!(name, "V{index}").unwrap();
399            }
400            InlineTypePathSegment::ArrayItem => name.push_str("Item"),
401            InlineTypePathSegment::MapValue => name.push_str("Value"),
402            InlineTypePathSegment::Optional => {
403                // Optional types are invisible for naming.
404            }
405            InlineTypePathSegment::Inherits(index) => {
406                write!(name, "P{index}").unwrap();
407            }
408        }
409    }
410
411    if name.is_empty() {
412        name.push_str("Value");
413    }
414
415    name
416}