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 = 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 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 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 {
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 !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#[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 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 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 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 }
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}