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