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#[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 #[inline]
30 pub fn new(cooked: CookedGraph<'a>) -> Self {
31 Self::with_config(cooked, &CodegenConfig::default())
32 }
33
34 #[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 #[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 #[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 #[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
90pub enum IdentMapping<'a> {
92 Type(TypeId),
94
95 Operation(&'a OperationId),
97
98 Path(&'a OperationId, &'a str),
100
101 Query(&'a OperationId, &'a str),
103
104 StructField(TypeId, StructFieldName<'a>),
106
107 EnumVariant(TypeId, &'a str),
109
110 TaggedVariant(TypeId, &'a str),
112
113 UntaggedVariant(TypeId, usize),
115
116 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
134fn 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 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 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 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 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 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 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 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 {
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 !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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
322enum InlineTypeIdentDomain<'a> {
323 Schema(TypeId),
324 Resource(ResourceGroup<'a>),
325}
326
327enum 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 }
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}