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::with_reserved(
306                    arena,
307                    &[&format!(
308                        "Other{}",
309                        CodegenIdentUsage::Type(idents[&IdentMapKey::Type(id)]).display()
310                    )],
311                );
312                for &variant in view.variants() {
313                    if let EnumVariant::String(name) = variant {
314                        map.insert(IdentMapKey::EnumVariant(id, name), scope.claim(name));
315                    }
316                }
317            }
318            Self::Tagged(id, view) => {
319                // Tagged variant names and common fields form different scopes:
320                // variant names must be unique within the generated enum;
321                // common fields are for naming inline types.
322                let mut scope = UniqueIdents::new(arena);
323                for variant in view.variants() {
324                    let name = variant.name();
325                    let ident = scope.claim(name);
326                    map.insert(IdentMapKey::TaggedVariant(id, name), ident);
327                }
328                let mut scope = UniqueIdents::new(arena);
329                for field in view.fields() {
330                    let name = field.name();
331                    let ident = match name {
332                        StructFieldName::Name(name) => scope.claim(name),
333                        StructFieldName::Ordinal(ordinal) => {
334                            let ident = idents[&IdentMapKey::Type(id)];
335                            scope.claim(&format!(
336                                "{}_{ordinal}",
337                                CodegenIdentUsage::Type(ident).display()
338                            ))
339                        }
340                        StructFieldName::AdditionalProperties => {
341                            scope.claim("additional_properties")
342                        }
343                    };
344                    map.insert(IdentMapKey::StructField(id, name), ident);
345                }
346            }
347            Self::Untagged(id, view) => {
348                let mut scope = UniqueIdents::new(arena);
349                for variant in view.variants() {
350                    use {ContainerView::*, InlineTypeView::*, TypeView::*};
351                    let ordinal = variant.ordinal();
352                    let ident = match variant.ty() {
353                        Some(Schema(schema)) => {
354                            let ident = idents[&IdentMapKey::Type(schema.id())];
355                            scope.adopt(ident)
356                        }
357                        Some(Inline(Primitive(_, primitive))) => {
358                            scope.claim(match primitive.ty() {
359                                PrimitiveType::String => "String",
360                                PrimitiveType::I8 => "I8",
361                                PrimitiveType::U8 => "U8",
362                                PrimitiveType::I16 => "I16",
363                                PrimitiveType::U16 => "U16",
364                                PrimitiveType::I32 => "I32",
365                                PrimitiveType::U32 => "U32",
366                                PrimitiveType::I64 => "I64",
367                                PrimitiveType::U64 => "U64",
368                                PrimitiveType::F32 => "F32",
369                                PrimitiveType::F64 => "F64",
370                                PrimitiveType::Bool => "Bool",
371                                PrimitiveType::DateTime => "DateTime",
372                                PrimitiveType::UnixTime => "UnixTime",
373                                PrimitiveType::Date => "Date",
374                                PrimitiveType::Url => "Url",
375                                PrimitiveType::Uuid => "Uuid",
376                                PrimitiveType::Bytes => "Bytes",
377                                PrimitiveType::Binary => "Binary",
378                            })
379                        }
380                        Some(Inline(Container(_, Array(_)))) => scope.claim("Array"),
381                        Some(Inline(Container(_, Map(_)))) => scope.claim("Map"),
382                        Some(Inline(..)) => {
383                            let ident = idents[&IdentMapKey::Type(id)];
384                            scope.claim(&format!(
385                                "{}_{ordinal}",
386                                CodegenIdentUsage::Type(ident).display()
387                            ))
388                        }
389                        None => scope.claim("None"),
390                    };
391                    map.insert(IdentMapKey::UntaggedVariant(id, ordinal), ident);
392                }
393                // Common fields inherited by all untagged variants.
394                let mut scope = UniqueIdents::new(arena);
395                for field in view.fields() {
396                    let name = field.name();
397                    let ident = match name {
398                        StructFieldName::Name(name) => scope.claim(name),
399                        StructFieldName::Ordinal(ordinal) => {
400                            let ident = idents[&IdentMapKey::Type(id)];
401                            scope.claim(&format!(
402                                "{}_{ordinal}",
403                                CodegenIdentUsage::Type(ident).display()
404                            ))
405                        }
406                        StructFieldName::AdditionalProperties => {
407                            scope.claim("additional_properties")
408                        }
409                    };
410                    map.insert(IdentMapKey::StructField(id, name), ident);
411                }
412            }
413        }
414        map
415    }
416}
417
418fn inline_type_candidate_name<'a>(
419    idents: &IdentMap<'a>,
420    path: &InlineTypePathView<'_, 'a>,
421) -> String {
422    let mut name = String::new();
423
424    for segment in path.segments() {
425        match segment {
426            InlineTypePathSegment::Field(parent, field) => {
427                let ident = idents[&IdentMapKey::StructField(parent, field)];
428                write!(name, "{}", CodegenIdentUsage::Type(ident).display()).unwrap();
429            }
430            InlineTypePathSegment::TaggedVariant(parent, variant) => {
431                let ident = idents[&IdentMapKey::TaggedVariant(parent, variant)];
432                write!(name, "{}", CodegenIdentUsage::Variant(ident).display()).unwrap();
433            }
434            InlineTypePathSegment::UntaggedVariant(parent, ordinal) => {
435                let ident = idents[&IdentMapKey::UntaggedVariant(parent, ordinal)];
436                write!(name, "{}", CodegenIdentUsage::Variant(ident).display()).unwrap();
437            }
438            InlineTypePathSegment::ArrayItem => name.push_str("Item"),
439            InlineTypePathSegment::MapValue => name.push_str("Value"),
440            InlineTypePathSegment::Optional => {
441                // Optional types are invisible for naming.
442            }
443            InlineTypePathSegment::Inherits(parent, ordinal) => {
444                let ident = idents[&IdentMapKey::Type(parent)];
445                write!(
446                    name,
447                    "{}_{ordinal}",
448                    CodegenIdentUsage::Type(ident).display()
449                )
450                .unwrap();
451            }
452        }
453    }
454
455    match path.root() {
456        InlineTypePathRoot::Schema(id) if name.is_empty() => {
457            let ident = idents[&IdentMapKey::Type(id)];
458            CodegenIdentUsage::Type(ident).display().to_string()
459        }
460        InlineTypePathRoot::Schema(..) => name,
461        InlineTypePathRoot::Operation { id, usage, .. } => {
462            let mut full = String::new();
463
464            let ident = idents[&IdentMapKey::Operation(id)];
465            write!(full, "{}", CodegenIdentUsage::Type(ident).display()).unwrap();
466            match usage {
467                OperationUsage::Path(param) => {
468                    let ident = idents[&IdentMapKey::Parameter(id, ParameterLocation::Path, param)];
469                    write!(full, "Path{}", CodegenIdentUsage::Type(ident).display()).unwrap();
470                }
471                OperationUsage::Query(param) => {
472                    let ident =
473                        idents[&IdentMapKey::Parameter(id, ParameterLocation::Query, param)];
474                    write!(full, "Query{}", CodegenIdentUsage::Type(ident).display()).unwrap();
475                }
476                OperationUsage::Request => full.push_str("Request"),
477                OperationUsage::Response => full.push_str("Response"),
478            }
479            full.push_str(&name);
480
481            full
482        }
483    }
484}