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`, `tracing`, and
150        // `trace-context` are special feature names.
151        let mut scope =
152            UniqueIdents::with_reserved(cooked.arena(), &["default", "tracing", "trace-context"]);
153        resources
154            .into_iter()
155            .map(move |name| (IdentMapKey::Resource(name), scope.claim(name)))
156    });
157    for op in cooked.operations() {
158        {
159            // Path parameters become arguments, so we need to reserve
160            // local variable and argument names that we use in the
161            // generated operation method body.
162            let mut scope = UniqueIdents::with_reserved(
163                cooked.arena(),
164                &["query", "request", "form", "url", "response"],
165            );
166            for param in op.path().params() {
167                let ident = scope.claim(param.name());
168                idents.insert(
169                    IdentMapKey::Parameter(op.id(), ParameterLocation::Path, param.name()),
170                    ident,
171                );
172            }
173        }
174        {
175            // Query parameters become regular struct fields.
176            let mut scope = UniqueIdents::new(cooked.arena());
177            for param in op.query() {
178                let ident = scope.claim(param.name());
179                idents.insert(
180                    IdentMapKey::Parameter(op.id(), ParameterLocation::Query, param.name()),
181                    ident,
182                );
183            }
184        }
185    }
186
187    for schema in cooked.schemas() {
188        if let Some(domain) = MemberIdentDomain::from_schema_type(schema) {
189            let map = domain.into_idents(cooked.arena(), &idents);
190            idents.extend(map);
191        }
192    }
193
194    // Inline type names depend on uniquified path segments. Build each inline
195    // type after its parent, then name its members for child path segments.
196    {
197        let inlines = cooked
198            .schemas()
199            .flat_map(|schema| schema.inlines())
200            .chain(cooked.operations().flat_map(|op| op.inlines()))
201            .filter(|ty| {
202                // Optional types are invisible for naming.
203                !matches!(ty, InlineTypeView::Container(_, ContainerView::Optional(_)))
204            });
205
206        let mut scopes = FxHashMap::default();
207        for inline in inlines {
208            let path = inline.path();
209            let domain = match path.root() {
210                InlineTypePathRoot::Schema(id) => InlineTypeIdentDomain::Schema(id),
211                InlineTypePathRoot::Operation { resource, .. } => InlineTypeIdentDomain::Resource(
212                    resource
213                        .map(|name| ResourceGroup::Named(idents[&IdentMapKey::Resource(name)]))
214                        .unwrap_or_default(),
215                ),
216            };
217            let name = inline_type_candidate_name(&idents, &path);
218            let scope = scopes
219                .entry(domain)
220                .or_insert_with(|| UniqueIdents::new(cooked.arena()));
221            idents.insert(IdentMapKey::Type(inline.id()), scope.claim(&name));
222            if let Some(domain) = MemberIdentDomain::from_inline_type(inline) {
223                let map = domain.into_idents(cooked.arena(), &idents);
224                idents.extend(map);
225            }
226        }
227    }
228    idents
229}
230
231type IdentMap<'a> = FxHashMap<IdentMapKey<'a>, UniqueIdent<'a>>;
232
233#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
234enum IdentMapKey<'a> {
235    Type(TypeId),
236    Operation(&'a OperationId),
237    Parameter(&'a OperationId, ParameterLocation, &'a str),
238    Resource(&'a str),
239    StructField(TypeId, StructFieldName<'a>),
240    EnumVariant(TypeId, &'a str),
241    TaggedVariant(TypeId, &'a str),
242    UntaggedVariant(TypeId, NonZeroUsize),
243}
244
245/// A uniqueness domain for inline type identifiers.
246#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
247enum InlineTypeIdentDomain<'a> {
248    Schema(TypeId),
249    Resource(ResourceGroup<'a>),
250}
251
252enum MemberIdentDomain<'graph, 'a> {
253    Struct(TypeId, StructView<'graph, 'a>),
254    Enum(TypeId, EnumView<'graph, 'a>),
255    Tagged(TypeId, TaggedView<'graph, 'a>),
256    Untagged(TypeId, UntaggedView<'graph, 'a>),
257}
258
259impl<'graph, 'a> MemberIdentDomain<'graph, 'a> {
260    fn from_schema_type(schema: SchemaTypeView<'graph, 'a>) -> Option<Self> {
261        let id = schema.id();
262        Some(match schema {
263            SchemaTypeView::Struct(_, view) => Self::Struct(id, view),
264            SchemaTypeView::Enum(_, view) => Self::Enum(id, view),
265            SchemaTypeView::Tagged(_, view) => Self::Tagged(id, view),
266            SchemaTypeView::Untagged(_, view) => Self::Untagged(id, view),
267            _ => return None,
268        })
269    }
270
271    fn from_inline_type(inline: InlineTypeView<'graph, 'a>) -> Option<Self> {
272        let id = inline.id();
273        Some(match inline {
274            InlineTypeView::Struct(_, view) => Self::Struct(id, view),
275            InlineTypeView::Enum(_, view) => Self::Enum(id, view),
276            InlineTypeView::Tagged(_, view) => Self::Tagged(id, view),
277            InlineTypeView::Untagged(_, view) => Self::Untagged(id, view),
278            _ => return None,
279        })
280    }
281
282    fn into_idents(self, arena: &'a Arena, idents: &IdentMap<'a>) -> IdentMap<'a> {
283        let mut map = IdentMap::default();
284        match self {
285            Self::Struct(id, view) => {
286                // Own, inherited, and synthesized struct fields.
287                let mut scope = UniqueIdents::new(arena);
288                for field in view.fields() {
289                    let name = field.name();
290                    let ident = match name {
291                        StructFieldName::Name(name) => scope.claim(name),
292                        StructFieldName::Ordinal(ordinal) => {
293                            let ident = idents[&IdentMapKey::Type(id)];
294                            scope.claim(&format!(
295                                "{}_{ordinal}",
296                                CodegenIdentUsage::Type(ident).display()
297                            ))
298                        }
299                        StructFieldName::AdditionalProperties => {
300                            scope.claim("additional_properties")
301                        }
302                    };
303                    map.insert(IdentMapKey::StructField(id, name), ident);
304                }
305            }
306            Self::Enum(id, view) => {
307                let mut scope = UniqueIdents::with_reserved(
308                    arena,
309                    &[&format!(
310                        "Other{}",
311                        CodegenIdentUsage::Type(idents[&IdentMapKey::Type(id)]).display()
312                    )],
313                );
314                for &variant in view.variants() {
315                    if let EnumVariant::String(name) = variant {
316                        map.insert(IdentMapKey::EnumVariant(id, name), scope.claim(name));
317                    }
318                }
319            }
320            Self::Tagged(id, view) => {
321                // Tagged variant names and common fields form different scopes:
322                // variant names must be unique within the generated enum;
323                // common fields are for naming inline types.
324                let mut scope = UniqueIdents::new(arena);
325                for variant in view.variants() {
326                    let name = variant.name();
327                    let ident = scope.claim(name);
328                    map.insert(IdentMapKey::TaggedVariant(id, name), ident);
329                }
330                let mut scope = UniqueIdents::new(arena);
331                for field in view.fields() {
332                    let name = field.name();
333                    let ident = match name {
334                        StructFieldName::Name(name) => scope.claim(name),
335                        StructFieldName::Ordinal(ordinal) => {
336                            let ident = idents[&IdentMapKey::Type(id)];
337                            scope.claim(&format!(
338                                "{}_{ordinal}",
339                                CodegenIdentUsage::Type(ident).display()
340                            ))
341                        }
342                        StructFieldName::AdditionalProperties => {
343                            scope.claim("additional_properties")
344                        }
345                    };
346                    map.insert(IdentMapKey::StructField(id, name), ident);
347                }
348            }
349            Self::Untagged(id, view) => {
350                let mut scope = UniqueIdents::new(arena);
351                for variant in view.variants() {
352                    use {ContainerView::*, InlineTypeView::*, TypeView::*};
353                    let ordinal = variant.ordinal();
354                    let ident = match variant.ty() {
355                        Some(Schema(schema)) => {
356                            let ident = idents[&IdentMapKey::Type(schema.id())];
357                            scope.adopt(ident)
358                        }
359                        Some(Inline(Primitive(_, primitive))) => {
360                            scope.claim(match primitive.ty() {
361                                PrimitiveType::String => "String",
362                                PrimitiveType::I8 => "I8",
363                                PrimitiveType::U8 => "U8",
364                                PrimitiveType::I16 => "I16",
365                                PrimitiveType::U16 => "U16",
366                                PrimitiveType::I32 => "I32",
367                                PrimitiveType::U32 => "U32",
368                                PrimitiveType::I64 => "I64",
369                                PrimitiveType::U64 => "U64",
370                                PrimitiveType::F32 => "F32",
371                                PrimitiveType::F64 => "F64",
372                                PrimitiveType::Bool => "Bool",
373                                PrimitiveType::DateTime => "DateTime",
374                                PrimitiveType::UnixTime => "UnixTime",
375                                PrimitiveType::Date => "Date",
376                                PrimitiveType::Url => "Url",
377                                PrimitiveType::Uuid => "Uuid",
378                                PrimitiveType::Bytes => "Bytes",
379                                PrimitiveType::Binary => "Binary",
380                            })
381                        }
382                        Some(Inline(Container(_, Array(_)))) => scope.claim("Array"),
383                        Some(Inline(Container(_, Map(_)))) => scope.claim("Map"),
384                        Some(Inline(..)) => {
385                            let ident = idents[&IdentMapKey::Type(id)];
386                            scope.claim(&format!(
387                                "{}_{ordinal}",
388                                CodegenIdentUsage::Type(ident).display()
389                            ))
390                        }
391                        None => scope.claim("None"),
392                    };
393                    map.insert(IdentMapKey::UntaggedVariant(id, ordinal), ident);
394                }
395                // Common fields inherited by all untagged variants.
396                let mut scope = UniqueIdents::new(arena);
397                for field in view.fields() {
398                    let name = field.name();
399                    let ident = match name {
400                        StructFieldName::Name(name) => scope.claim(name),
401                        StructFieldName::Ordinal(ordinal) => {
402                            let ident = idents[&IdentMapKey::Type(id)];
403                            scope.claim(&format!(
404                                "{}_{ordinal}",
405                                CodegenIdentUsage::Type(ident).display()
406                            ))
407                        }
408                        StructFieldName::AdditionalProperties => {
409                            scope.claim("additional_properties")
410                        }
411                    };
412                    map.insert(IdentMapKey::StructField(id, name), ident);
413                }
414            }
415        }
416        map
417    }
418}
419
420fn inline_type_candidate_name<'a>(
421    idents: &IdentMap<'a>,
422    path: &InlineTypePathView<'_, 'a>,
423) -> String {
424    let mut name = String::new();
425
426    for segment in path.segments() {
427        match segment {
428            InlineTypePathSegment::Field(parent, field) => {
429                let ident = idents[&IdentMapKey::StructField(parent, field)];
430                write!(name, "{}", CodegenIdentUsage::Type(ident).display()).unwrap();
431            }
432            InlineTypePathSegment::TaggedVariant(parent, variant) => {
433                let ident = idents[&IdentMapKey::TaggedVariant(parent, variant)];
434                write!(name, "{}", CodegenIdentUsage::Variant(ident).display()).unwrap();
435            }
436            InlineTypePathSegment::UntaggedVariant(parent, ordinal) => {
437                let ident = idents[&IdentMapKey::UntaggedVariant(parent, ordinal)];
438                write!(name, "{}", CodegenIdentUsage::Variant(ident).display()).unwrap();
439            }
440            InlineTypePathSegment::ArrayItem => name.push_str("Item"),
441            InlineTypePathSegment::MapValue => name.push_str("Value"),
442            InlineTypePathSegment::Optional => {
443                // Optional types are invisible for naming.
444            }
445            InlineTypePathSegment::Inherits(parent, ordinal) => {
446                let ident = idents[&IdentMapKey::Type(parent)];
447                write!(
448                    name,
449                    "{}_{ordinal}",
450                    CodegenIdentUsage::Type(ident).display()
451                )
452                .unwrap();
453            }
454        }
455    }
456
457    match path.root() {
458        InlineTypePathRoot::Schema(id) if name.is_empty() => {
459            let ident = idents[&IdentMapKey::Type(id)];
460            CodegenIdentUsage::Type(ident).display().to_string()
461        }
462        InlineTypePathRoot::Schema(..) => name,
463        InlineTypePathRoot::Operation { id, usage, .. } => {
464            let mut full = String::new();
465
466            let ident = idents[&IdentMapKey::Operation(id)];
467            write!(full, "{}", CodegenIdentUsage::Type(ident).display()).unwrap();
468            match usage {
469                OperationUsage::Path(param) => {
470                    let ident = idents[&IdentMapKey::Parameter(id, ParameterLocation::Path, param)];
471                    write!(full, "Path{}", CodegenIdentUsage::Type(ident).display()).unwrap();
472                }
473                OperationUsage::Query(param) => {
474                    let ident =
475                        idents[&IdentMapKey::Parameter(id, ParameterLocation::Query, param)];
476                    write!(full, "Query{}", CodegenIdentUsage::Type(ident).display()).unwrap();
477                }
478                OperationUsage::Request => full.push_str("Request"),
479                OperationUsage::Response => full.push_str("Response"),
480            }
481            full.push_str(&name);
482
483            full
484        }
485    }
486}