Skip to main content

ploidy_codegen_rust/
graph.rs

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