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#[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 #[inline]
31 pub fn new(cooked: CookedGraph<'a>) -> Self {
32 Self::with_config(cooked, &CodegenConfig::default())
33 }
34
35 #[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 #[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 #[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 #[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
88pub enum IdentMapping<'a> {
90 Type(TypeId),
92 Operation(&'a OperationId),
94 Path(&'a OperationId, &'a str),
96 Query(&'a OperationId, &'a str),
98 StructField(TypeId, StructFieldName<'a>),
100 EnumVariant(TypeId, &'a str),
102 TaggedVariant(TypeId, &'a str),
104 UntaggedVariant(TypeId, NonZeroUsize),
106 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
124fn 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 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 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 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 {
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 !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#[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 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 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 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 }
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}