1use std::{
2 borrow::Cow,
3 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
4 fmt,
5 ops::Deref,
6 panic::Location,
7 path::{Path, PathBuf},
8 sync::Arc,
9};
10
11use specta::{
12 Format, Types,
13 datatype::{DataType, Fields, NamedDataType, NamedReference, NamedReferenceType, Reference},
14};
15
16use crate::{Branded, Error, primitives, references};
17
18fn rust_type_path(ndt: &NamedDataType) -> Cow<'static, str> {
19 if ndt.module_path.is_empty() {
20 ndt.name.clone()
21 } else {
22 Cow::Owned(format!("{}::{}", ndt.module_path, ndt.name))
23 }
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28pub enum Layout {
29 Namespaces,
31 Files,
33 ModulePrefixedName,
35 #[default]
38 FlatFile,
39}
40
41impl fmt::Display for Layout {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{self:?}")
44 }
45}
46
47#[derive(Clone)]
48#[allow(clippy::type_complexity)]
49struct RuntimeFn(Arc<dyn Fn(FrameworkExporter) -> Result<Cow<'static, str>, Error> + Send + Sync>);
50
51impl fmt::Debug for RuntimeFn {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "RuntimeFn({:p})", self.0)
54 }
55}
56
57#[derive(Clone)]
58#[allow(clippy::type_complexity)]
59pub struct BrandedTypeImpl(
60 pub(crate) Arc<
61 dyn for<'a> Fn(BrandedTypeExporter<'a>, &Branded) -> Result<Cow<'static, str>, Error>
62 + Send
63 + Sync,
64 >,
65);
66
67impl fmt::Debug for BrandedTypeImpl {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "BrandedTypeImpl({:p})", self.0)
70 }
71}
72
73#[derive(Debug, Clone)]
75#[non_exhaustive]
76pub struct Exporter {
77 pub header: Cow<'static, str>,
79 framework_runtime: Option<RuntimeFn>,
80 pub(crate) branded_type_impl: Option<BrandedTypeImpl>,
81 framework_prelude: Cow<'static, str>,
82 pub layout: Layout,
84 pub(crate) jsdoc: bool,
85}
86
87impl Exporter {
88 pub(crate) fn default() -> Exporter {
90 Exporter {
91 header: Cow::Borrowed(""),
92 framework_runtime: None,
93 branded_type_impl: None,
94 framework_prelude: Cow::Borrowed(
95 "// This file has been generated by Specta. Do not edit this file manually.",
96 ),
97 layout: Default::default(),
98 jsdoc: false,
99 }
100 }
101
102 pub fn framework_prelude(mut self, prelude: impl Into<Cow<'static, str>>) -> Self {
104 self.framework_prelude = prelude.into();
105 self
106 }
107
108 pub fn framework_runtime(
114 mut self,
115 builder: impl Fn(FrameworkExporter) -> Result<Cow<'static, str>, Error> + Send + Sync + 'static,
116 ) -> Self {
117 self.framework_runtime = Some(RuntimeFn(Arc::new(builder)));
118 self
119 }
120
121 pub fn branded_type_impl(
161 mut self,
162 builder: impl for<'a> Fn(BrandedTypeExporter<'a>, &Branded) -> Result<Cow<'static, str>, Error>
163 + Send
164 + Sync
165 + 'static,
166 ) -> Self {
167 self.branded_type_impl = Some(BrandedTypeImpl(Arc::new(builder)));
168 self
169 }
170
171 pub fn header(mut self, header: impl Into<Cow<'static, str>>) -> Self {
175 self.header = header.into();
176 self
177 }
178
179 pub fn layout(mut self, layout: Layout) -> Self {
181 self.layout = layout;
182 self
183 }
184
185 pub fn export(&self, types: &Types, format: impl Format) -> Result<String, Error> {
189 fn inner(exporter: Exporter, types: &Types, format: &dyn Format) -> Result<String, Error> {
190 let types = format_types(types, &format)?;
191 let types = types.as_ref();
192
193 if let Layout::Files = exporter.layout {
194 return Err(Error::export_requires_export_to(exporter.layout));
195 }
196 if let Layout::Namespaces = exporter.layout
197 && exporter.jsdoc
198 {
199 return Err(Error::jsdoc_namespaces_unsupported());
200 }
201
202 let mut out = render_file_header(&exporter)?;
203
204 let mut has_manually_exported_user_types = false;
205 let mut runtime = Ok(Cow::default());
206 if let Some(framework_runtime) = &exporter.framework_runtime {
207 runtime = (framework_runtime.0)(FrameworkExporter {
208 exporter: &exporter,
209 format: Some(&format),
210 has_manually_exported_user_types: &mut has_manually_exported_user_types,
211 files_root_types: "",
212 types,
213 });
214 }
215 let runtime = runtime?;
216
217 if !runtime.is_empty() {
219 out += "\n";
220 }
221 out += &runtime;
222 if !runtime.is_empty() {
223 out += "\n";
224 }
225
226 if !has_manually_exported_user_types {
228 render_types(&mut out, &exporter, Some(&format), types, "")?;
229 }
230
231 Ok(out)
232 }
233
234 inner(self.clone(), types, &format)
235 }
236
237 pub fn export_to(
243 &self,
244 path: impl AsRef<Path>,
245 types: &Types,
246 format: impl Format,
247 ) -> Result<(), Error> {
248 fn inner(
249 exporter: Exporter,
250 path: &Path,
251 types: &Types,
252 format: &dyn Format,
253 ) -> Result<(), Error> {
254 let formatted_types = format_types(types, &format)?;
255 let types = formatted_types.as_ref();
256
257 if exporter.layout != Layout::Files {
258 let mut result = render_file_header(&exporter)?;
259
260 let mut has_manually_exported_user_types = false;
261 let mut runtime = Ok(Cow::default());
262 if let Some(framework_runtime) = &exporter.framework_runtime {
263 runtime = (framework_runtime.0)(FrameworkExporter {
264 exporter: &exporter,
265 format: Some(&format),
266 has_manually_exported_user_types: &mut has_manually_exported_user_types,
267 files_root_types: "",
268 types,
269 });
270 }
271 let runtime = runtime?;
272
273 if !runtime.is_empty() {
274 result.push('\n');
275 result.push_str(&runtime);
276 result.push('\n');
277 }
278
279 if !has_manually_exported_user_types {
280 render_types(&mut result, &exporter, Some(&format), types, "")?;
281 }
282
283 if let Some(parent) = path.parent() {
284 std::fs::create_dir_all(parent)
285 .map_err(|source| Error::create_dir(parent.to_path_buf(), source))?;
286 };
287 std::fs::write(path, result)
288 .map_err(|source| Error::write_file(path.to_path_buf(), source))?;
289 return Ok(());
290 }
291
292 fn export(
293 exporter: &Exporter,
294 format: Option<&dyn Format>,
295 types: &Types,
296 module: &mut Module,
297 s: &mut String,
298 path: &Path,
299 files: &mut HashMap<PathBuf, String>,
300 ) -> Result<bool, Error> {
301 module.types.sort_by(|a, b| {
302 a.name
303 .cmp(&b.name)
304 .then(a.module_path.cmp(&b.module_path))
305 .then(a.location.cmp(&b.location))
306 });
307 let (rendered_types_result, referenced_types) =
308 references::with_module_path(module.module_path.as_ref(), || {
309 references::collect_references(|| {
310 let mut rendered = String::new();
311 let exports = render_flat_types(
312 &mut rendered,
313 exporter,
314 format,
315 types,
316 module.types.iter().copied(),
317 "",
318 )?;
319 Ok::<_, Error>((rendered, exports))
320 })
321 });
322 let (rendered_types, exports) = rendered_types_result?;
323
324 let import_paths = referenced_types
325 .into_iter()
326 .map(|r| reference_module_path(types, &r))
327 .collect::<Result<Vec<_>, _>>()?
328 .into_iter()
329 .flatten()
330 .filter(|module_path| module_path != module.module_path.as_ref())
331 .collect::<BTreeSet<_>>();
332 if !import_paths.is_empty() {
333 s.push('\n');
334 s.push_str(&module_import_block(
335 exporter,
336 module.module_path.as_ref(),
337 &import_paths,
338 ));
339 }
340
341 if !import_paths.is_empty() && !rendered_types.is_empty() {
342 s.push('\n');
343 }
344
345 s.push_str(&rendered_types);
346
347 for (name, module) in &mut module.children {
348 if module.types.is_empty() && module.children.is_empty() {
351 continue;
352 }
353
354 let mut path = path.join(name);
355 let mut out = render_file_header(exporter)?;
356
357 let has_types =
358 export(exporter, format, types, module, &mut out, &path, files)?;
359 if has_types {
360 path.set_extension(if exporter.jsdoc { "js" } else { "ts" });
361 files.insert(path, out);
362 }
363 }
364
365 Ok(!exports.is_empty())
366 }
367
368 let mut files = HashMap::new();
369 let mut runtime_path = path.join("index");
370 runtime_path.set_extension(if exporter.jsdoc { "js" } else { "ts" });
371
372 let mut root_types = String::new();
373 export(
374 &exporter,
375 Some(&format),
376 types,
377 &mut build_module_graph(types),
378 &mut root_types,
379 path,
380 &mut files,
381 )?;
382
383 {
384 let mut has_manually_exported_user_types = false;
385 let mut runtime = Cow::default();
386 let mut runtime_references = HashSet::new();
387 if let Some(framework_runtime) = &exporter.framework_runtime {
388 let (runtime_result, referenced_types) =
389 references::with_module_path("", || {
390 references::collect_references(|| {
391 (framework_runtime.0)(FrameworkExporter {
392 exporter: &exporter,
393 format: Some(&format),
394 has_manually_exported_user_types:
395 &mut has_manually_exported_user_types,
396 files_root_types: &root_types,
397 types,
398 })
399 })
400 });
401 runtime = runtime_result?;
402 runtime_references = referenced_types;
403 }
404
405 let should_export_user_types =
406 !has_manually_exported_user_types && !root_types.is_empty();
407
408 if !runtime.is_empty() || should_export_user_types {
409 files.insert(runtime_path, {
410 let mut out = render_file_header(&exporter)?;
411 let mut body = String::new();
412
413 if !runtime.is_empty() {
415 body.push_str(&runtime);
416 }
417
418 if should_export_user_types {
420 if !body.is_empty() {
421 body.push('\n');
422 }
423
424 body.push_str(&root_types);
425 }
426
427 let import_paths = runtime_references
428 .into_iter()
429 .map(|r| reference_module_path(types, &r))
430 .collect::<Result<Vec<_>, _>>()?
431 .into_iter()
432 .flatten()
433 .filter(|module_path| !module_path.is_empty())
434 .collect::<BTreeSet<_>>();
435
436 let import_paths = import_paths
437 .into_iter()
438 .filter(|module_path| {
439 !body.contains(&module_import_statement(&exporter, "", module_path))
440 })
441 .collect::<BTreeSet<_>>();
442
443 if !import_paths.is_empty() {
444 out.push('\n');
445 out.push_str(&module_import_block(&exporter, "", &import_paths));
446 }
447
448 if !body.is_empty() {
449 out.push('\n');
450 if !import_paths.is_empty() {
451 out.push('\n');
452 }
453 out.push_str(&body);
454 }
455
456 out
457 });
458 }
459 }
460
461 match path.metadata() {
462 Ok(meta) if !meta.is_dir() => std::fs::remove_file(path).or_else(|source| {
463 if source.kind() == std::io::ErrorKind::NotFound {
464 Ok(())
465 } else {
466 Err(Error::remove_file(path.to_path_buf(), source))
467 }
468 })?,
469 Ok(_) => {}
470 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
471 Err(source) => {
472 return Err(Error::metadata(path.to_path_buf(), source));
473 }
474 }
475
476 for (path, content) in &files {
477 if let Some(parent) = path.parent() {
478 std::fs::create_dir_all(parent)
479 .map_err(|source| Error::create_dir(parent.to_path_buf(), source))?;
480 }
481 std::fs::write(path, content)
482 .map_err(|source| Error::write_file(path.clone(), source))?;
483 }
484
485 cleanup_stale_files(path, &files, &exporter)?;
486
487 Ok(())
488 }
489
490 inner(self.clone(), path.as_ref(), types, &format)
491 }
492}
493
494fn reference_module_path(types: &Types, r: &NamedReference) -> Result<Option<String>, Error> {
495 match &r.inner {
496 NamedReferenceType::Reference { .. } => types
497 .get(r)
498 .map(|ndt| Some(ndt.module_path.as_ref().to_string()))
499 .ok_or_else(|| {
500 Error::dangling_named_reference("import resolution".to_string(), format!("{r:?}"))
501 }),
502 NamedReferenceType::Inline { .. } => Ok(None),
503 NamedReferenceType::Recursive(_) => types
504 .get(r)
505 .map(|ndt| Some(ndt.module_path.as_ref().to_string()))
506 .ok_or_else(|| {
507 Error::dangling_named_reference("import resolution".to_string(), format!("{r:?}"))
508 }),
509 }
510}
511
512fn format_types<'a>(types: &'a Types, format: &dyn Format) -> Result<Cow<'a, Types>, Error> {
513 Ok(
514 match format
515 .map_types(types)
516 .map_err(|err| Error::format("type graph formatter failed", err))?
517 {
518 Cow::Borrowed(_) => Cow::Borrowed(types),
519 Cow::Owned(types) => Cow::Owned(types),
520 },
521 )
522}
523
524fn map_datatype_format(
525 format: Option<&dyn Format>,
526 types: &Types,
527 dt: &DataType,
528 path: &[Cow<'static, str>],
529) -> Result<DataType, Error> {
530 if matches!(dt, DataType::Generic(_)) {
531 return Ok(dt.clone());
532 }
533
534 fn contains_generic_reference(dt: &DataType) -> Result<bool, Error> {
535 Ok(match dt {
536 DataType::Primitive(_) => false,
537 DataType::List(list) => contains_generic_reference(&list.ty)?,
538 DataType::Map(map) => {
539 contains_generic_reference(map.key_ty())?
540 || contains_generic_reference(map.value_ty())?
541 }
542 DataType::Nullable(inner) => contains_generic_reference(inner)?,
543 DataType::Struct(strct) => match &strct.fields {
544 Fields::Unit => false,
545 Fields::Unnamed(unnamed) => unnamed
546 .fields
547 .iter()
548 .filter_map(|field| field.ty.as_ref())
549 .try_fold(false, |found, ty| {
550 Ok::<_, Error>(found || contains_generic_reference(ty)?)
551 })?,
552 Fields::Named(named) => named
553 .fields
554 .iter()
555 .filter_map(|(_, field)| field.ty.as_ref())
556 .try_fold(false, |found, ty| {
557 Ok::<_, Error>(found || contains_generic_reference(ty)?)
558 })?,
559 },
560 DataType::Enum(enm) => enm.variants.iter().try_fold(false, |found, (_, variant)| {
561 let variant_found = match &variant.fields {
562 Fields::Unit => false,
563 Fields::Unnamed(unnamed) => unnamed
564 .fields
565 .iter()
566 .filter_map(|field| field.ty.as_ref())
567 .try_fold(false, |found, ty| {
568 Ok::<_, Error>(found || contains_generic_reference(ty)?)
569 })?,
570 Fields::Named(named) => named
571 .fields
572 .iter()
573 .filter_map(|(_, field)| field.ty.as_ref())
574 .try_fold(false, |found, ty| {
575 Ok::<_, Error>(found || contains_generic_reference(ty)?)
576 })?,
577 };
578
579 Ok::<_, Error>(found || variant_found)
580 })?,
581 DataType::Tuple(tuple) => tuple.elements.iter().try_fold(false, |found, ty| {
582 Ok::<_, Error>(found || contains_generic_reference(ty)?)
583 })?,
584 DataType::Reference(Reference::Named(reference)) => match &reference.inner {
585 NamedReferenceType::Reference { generics, .. } => {
586 generics.iter().try_fold(false, |found, (_, dt)| {
587 Ok::<_, Error>(found || contains_generic_reference(dt)?)
588 })?
589 }
590 NamedReferenceType::Inline { .. } => false,
591 NamedReferenceType::Recursive(_) => false,
592 },
593 DataType::Generic(_) => true,
594 DataType::Reference(Reference::Opaque(_)) => false,
595 DataType::Intersection(types) => types.iter().try_fold(false, |found, ty| {
596 Ok::<_, Error>(found || contains_generic_reference(ty)?)
597 })?,
598 })
599 }
600
601 if contains_generic_reference(dt)? {
602 let Some(format) = format else {
603 return map_datatype_format_children(None, types, dt.clone(), path);
604 };
605
606 match format.map_type(types, dt) {
607 Ok(Cow::Borrowed(dt)) => {
608 return map_datatype_format_children(Some(format), types, dt.clone(), path);
609 }
610 Ok(Cow::Owned(dt)) => {
611 return map_datatype_format_children(Some(format), types, dt, path);
612 }
613 Err(err) if is_unresolved_generic_format_error(err.as_ref()) => {
614 return map_datatype_format_children(Some(format), types, dt.clone(), path);
615 }
616 Err(err) => {
617 return Err(Error::format_at(
618 "datatype formatter failed",
619 path.join("."),
620 err,
621 ));
622 }
623 }
624 }
625
626 let Some(format) = format else {
627 return Ok(dt.clone());
628 };
629
630 let mapped = format
631 .map_type(types, dt)
632 .map_err(|err| Error::format_at("datatype formatter failed", path.join("."), err))?;
633
634 match mapped {
635 Cow::Borrowed(dt) => map_datatype_format_children(Some(format), types, dt.clone(), path),
636 Cow::Owned(dt) => map_datatype_format_children(Some(format), types, dt, path),
637 }
638}
639
640fn is_unresolved_generic_format_error(err: &(dyn std::error::Error + 'static)) -> bool {
641 err.to_string().contains("Unresolved generic reference")
645}
646
647fn map_datatype_format_children(
648 format: Option<&dyn Format>,
649 types: &Types,
650 mut dt: DataType,
651 path: &[Cow<'static, str>],
652) -> Result<DataType, Error> {
653 match &mut dt {
654 DataType::Primitive(_) => {}
655 DataType::List(list) => {
656 let child_path = format_path(path, "<list_item>");
657 *list.ty = map_datatype_format(format, types, &list.ty, &child_path)?;
658 }
659 DataType::Map(map) => {
660 let key_path = format_path(path, "<map_key>");
661 let value_path = format_path(path, "<map_value>");
662 let key = map_datatype_format(format, types, map.key_ty(), &key_path)?;
663 let value = map_datatype_format(format, types, map.value_ty(), &value_path)?;
664 map.set_key_ty(key);
665 map.set_value_ty(value);
666 }
667 DataType::Nullable(inner) => {
668 **inner = map_datatype_format(format, types, inner, path)?;
669 }
670 DataType::Struct(strct) => map_datatype_fields(format, types, &mut strct.fields, path)?,
671 DataType::Enum(enm) => {
672 for (variant_name, variant) in &mut enm.variants {
673 let variant_path = format_path(path, variant_name.clone());
674 map_datatype_fields(format, types, &mut variant.fields, &variant_path)?;
675 }
676 }
677 DataType::Tuple(tuple) => {
678 for (idx, element) in tuple.elements.iter_mut().enumerate() {
679 let element_path = format_path(path, idx.to_string());
680 *element = map_datatype_format(format, types, element, &element_path)?;
681 }
682 }
683 DataType::Intersection(types_) => {
684 for ty in types_ {
685 *ty = map_datatype_format(format, types, ty, path)?;
686 }
687 }
688 DataType::Reference(Reference::Named(reference)) => {
689 if let NamedReferenceType::Inline { dt, .. } = &mut reference.inner {
690 **dt = map_datatype_format(format, types, dt, path)?;
691 }
692
693 for (_, dt) in named_reference_generics_mut(reference) {
694 *dt = map_datatype_format(format, types, dt, path)?;
695 }
696 }
697 DataType::Generic(_) => {}
698 DataType::Reference(Reference::Opaque(reference)) => {
699 if let Some(branded) = reference.downcast_ref::<Branded>() {
700 dt = Reference::opaque(Branded::new(
701 branded.brand().clone(),
702 map_datatype_format(format, types, branded.ty(), path)?,
703 ))
704 .into();
705 }
706 }
707 }
708
709 Ok(dt)
710}
711
712fn format_path(
713 path: &[Cow<'static, str>],
714 segment: impl Into<Cow<'static, str>>,
715) -> Vec<Cow<'static, str>> {
716 let mut path = path.to_vec();
717 path.push(segment.into());
718 path
719}
720
721fn named_reference_generics_mut(
722 reference: &mut NamedReference,
723) -> &mut [(specta::datatype::Generic, DataType)] {
724 match &mut reference.inner {
725 NamedReferenceType::Reference { generics, .. } => generics,
726 NamedReferenceType::Inline { .. } | NamedReferenceType::Recursive(_) => &mut [],
727 }
728}
729
730fn map_datatype_fields(
731 format: Option<&dyn Format>,
732 types: &Types,
733 fields: &mut Fields,
734 path: &[Cow<'static, str>],
735) -> Result<(), Error> {
736 match fields {
737 Fields::Unit => {}
738 Fields::Unnamed(unnamed) => {
739 for (idx, field) in unnamed.fields.iter_mut().enumerate() {
740 if let Some(ty) = field.ty.as_mut() {
741 let field_path = format_path(path, idx.to_string());
742 *ty = map_datatype_format(format, types, ty, &field_path)?;
743 }
744 }
745 }
746 Fields::Named(named) => {
747 for (name, field) in &mut named.fields {
748 if let Some(ty) = field.ty.as_mut() {
749 let field_path = format_path(path, name.clone());
750 *ty = map_datatype_format(format, types, ty, &field_path)?;
751 }
752 }
753 }
754 }
755
756 Ok(())
757}
758
759fn map_named_datatype_format(
760 format: Option<&dyn Format>,
761 types: &Types,
762 ndt: &NamedDataType,
763) -> Result<NamedDataType, Error> {
764 let mut mapped = ndt.clone();
765 mapped.ty = ndt
766 .ty
767 .clone()
768 .map(|ty| map_datatype_format_children(format, types, ty, &[rust_type_path(ndt)]))
769 .transpose()
770 .map_err(|err| err.with_named_datatype(ndt))?;
771 Ok(mapped)
772}
773
774impl AsRef<Exporter> for Exporter {
775 fn as_ref(&self) -> &Exporter {
776 self
777 }
778}
779
780impl AsMut<Exporter> for Exporter {
781 fn as_mut(&mut self) -> &mut Exporter {
782 self
783 }
784}
785
786pub struct BrandedTypeExporter<'a> {
788 pub(crate) exporter: &'a Exporter,
789 pub(crate) format: Option<&'a dyn Format>,
790 pub types: &'a Types,
792}
793
794impl fmt::Debug for BrandedTypeExporter<'_> {
795 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
796 self.exporter.fmt(f)
797 }
798}
799
800impl AsRef<Exporter> for BrandedTypeExporter<'_> {
801 fn as_ref(&self) -> &Exporter {
802 self
803 }
804}
805
806impl Deref for BrandedTypeExporter<'_> {
807 type Target = Exporter;
808
809 fn deref(&self) -> &Self::Target {
810 self.exporter
811 }
812}
813
814impl BrandedTypeExporter<'_> {
815 pub fn inline(&self, dt: &DataType) -> Result<String, Error> {
817 let mapped = map_datatype_format(self.format, self.types, dt, &[])?;
818 primitives::inline(self, self.types, &mapped)
819 }
820
821 pub fn reference(&self, r: &Reference) -> Result<String, Error> {
823 let mapped = map_datatype_format(
824 self.format,
825 self.types,
826 &DataType::Reference(r.clone()),
827 &[],
828 )?;
829 match mapped {
830 DataType::Reference(reference) => primitives::reference(self, self.types, &reference),
831 dt => primitives::inline(self, self.types, &dt),
832 }
833 }
834}
835
836pub struct FrameworkExporter<'a> {
838 exporter: &'a Exporter,
839 format: Option<&'a dyn Format>,
840 has_manually_exported_user_types: &'a mut bool,
841 files_root_types: &'a str,
843 pub types: &'a Types,
845}
846
847impl fmt::Debug for FrameworkExporter<'_> {
848 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849 self.exporter.fmt(f)
850 }
851}
852
853impl AsRef<Exporter> for FrameworkExporter<'_> {
854 fn as_ref(&self) -> &Exporter {
855 self
856 }
857}
858
859impl Deref for FrameworkExporter<'_> {
860 type Target = Exporter;
861
862 fn deref(&self) -> &Self::Target {
863 self.exporter
864 }
865}
866
867impl FrameworkExporter<'_> {
868 pub fn render_types(&mut self) -> Result<Cow<'static, str>, Error> {
873 let mut s = String::new();
874 render_types(
875 &mut s,
876 self.exporter,
877 self.format,
878 self.types,
879 self.files_root_types,
880 )?;
881 *self.has_manually_exported_user_types = true;
882 Ok(Cow::Owned(s))
883 }
884
885 pub fn inline(&self, dt: &DataType) -> Result<String, Error> {
887 let mapped = map_datatype_format(self.format, self.types, dt, &[])?;
888 primitives::inline(self, self.types, &mapped)
889 }
890
891 pub fn reference(&self, r: &Reference) -> Result<String, Error> {
893 let mapped = map_datatype_format(
894 self.format,
895 self.types,
896 &DataType::Reference(r.clone()),
897 &[],
898 )?;
899 match mapped {
900 DataType::Reference(reference) => primitives::reference(self, self.types, &reference),
901 dt => primitives::inline(self, self.types, &dt),
902 }
903 }
904
905 pub fn export<'a>(
907 &self,
908 ndts: impl Iterator<Item = &'a NamedDataType>,
909 indent: &'a str,
910 ) -> Result<String, Error> {
911 let mapped = ndts
912 .map(|ndt| map_named_datatype_format(self.format, self.types, ndt))
913 .collect::<Result<Vec<_>, _>>()?;
914 primitives::export(self, self.types, mapped.iter(), indent)
915 }
916}
917
918struct Module<'a> {
919 types: Vec<&'a NamedDataType>,
920 children: BTreeMap<&'a str, Module<'a>>,
921 module_path: Cow<'static, str>,
922}
923
924fn build_module_graph(types: &Types) -> Module<'_> {
925 types.into_unsorted_iter().fold(
926 Module {
927 types: Default::default(),
928 children: Default::default(),
929 module_path: Default::default(),
930 },
931 |mut ns, ndt| {
932 let path = &ndt.module_path;
933
934 if path.is_empty() {
935 ns.types.push(ndt);
936 } else {
937 let mut current = &mut ns;
938 let mut current_path = String::new();
939 for segment in path.split("::") {
940 if !current_path.is_empty() {
941 current_path.push_str("::");
942 }
943 current_path.push_str(segment);
944
945 current = current.children.entry(segment).or_insert_with(|| Module {
946 types: Default::default(),
947 children: Default::default(),
948 module_path: current_path.clone().into(),
949 });
950 }
951
952 current.types.push(ndt);
953 }
954
955 ns
956 },
957 )
958}
959
960fn render_file_header(exporter: &Exporter) -> Result<String, Error> {
961 let mut out = exporter.header.to_string();
962 if !exporter.header.is_empty() {
963 out += "\n";
964 }
965
966 out += &exporter.framework_prelude;
967 if !exporter.framework_prelude.is_empty() {
968 out += "\n";
969 }
970
971 Ok(out)
972}
973
974fn render_types(
975 s: &mut String,
976 exporter: &Exporter,
977 format: Option<&dyn Format>,
978 types: &Types,
979 files_user_types: &str,
980) -> Result<(), Error> {
981 match exporter.layout {
982 Layout::Namespaces => {
983 fn has_renderable_content(module: &Module<'_>) -> bool {
984 module.types.iter().any(|ndt| ndt.ty.is_some())
985 || module.children.values().any(has_renderable_content)
986 }
987
988 fn export<'a>(
989 exporter: &Exporter,
990 format: Option<&dyn Format>,
991 types: &Types,
992 s: &mut String,
993 module: impl ExactSizeIterator<Item = (&'a &'a str, &'a mut Module<'a>)>,
994 depth: usize,
995 ) -> Result<(), Error> {
996 let namespace_indent = "\t".repeat(depth);
997 let content_indent = "\t".repeat(depth + 1);
998
999 for (name, module) in module {
1000 if !has_renderable_content(module) {
1001 continue;
1002 }
1003
1004 s.push('\n');
1005 s.push_str(&namespace_indent);
1006 if depth != 0 && *name != "$specta$" {
1007 s.push_str("export ");
1008 }
1009 s.push_str("namespace ");
1010 s.push_str(name);
1011 s.push_str(" {\n");
1012
1013 module.types.sort_by(|a, b| {
1015 a.name
1016 .cmp(&b.name)
1017 .then(a.module_path.cmp(&b.module_path))
1018 .then(a.location.cmp(&b.location))
1019 });
1020 render_flat_types(
1021 s,
1022 exporter,
1023 format,
1024 types,
1025 module.types.iter().copied(),
1026 &content_indent,
1027 )?;
1028
1029 export(
1031 exporter,
1032 format,
1033 types,
1034 s,
1035 module.children.iter_mut(),
1036 depth + 1,
1037 )?;
1038
1039 s.push_str(&namespace_indent);
1040 s.push_str("}\n");
1041 }
1042
1043 Ok(())
1044 }
1045
1046 let mut module = build_module_graph(types);
1047
1048 let reexports = {
1049 let mut reexports = String::new();
1050 for name in module
1051 .children
1052 .iter()
1053 .filter_map(|(name, module)| has_renderable_content(module).then_some(*name))
1054 .chain(
1055 module
1056 .types
1057 .iter()
1058 .filter(|ndt| ndt.ty.is_some())
1059 .map(|ndt| ndt.name.as_ref()),
1060 )
1061 {
1062 reexports.push_str("export import ");
1063 reexports.push_str(name);
1064 reexports.push_str(" = $s$.");
1065 reexports.push_str(name);
1066 reexports.push_str(";\n");
1067 }
1068 reexports
1069 };
1070
1071 export(
1072 exporter,
1073 format,
1074 types,
1075 s,
1076 [(&"$s$", &mut module)].into_iter(),
1077 0,
1078 )?;
1079 s.push_str(&reexports);
1080 }
1081 Layout::ModulePrefixedName | Layout::FlatFile => {
1082 render_flat_types(s, exporter, format, types, types.into_sorted_iter(), "")?;
1083 }
1084 Layout::Files => {
1087 if !files_user_types.is_empty() {
1088 s.push_str(files_user_types);
1089 }
1090 }
1091 }
1092
1093 Ok(())
1094}
1095
1096fn render_flat_types<'a>(
1099 s: &mut String,
1100 exporter: &Exporter,
1101 format: Option<&dyn Format>,
1102 types: &Types,
1103 ndts: impl ExactSizeIterator<Item = &'a NamedDataType>,
1104 indent: &str,
1105) -> Result<HashMap<String, Location<'static>>, Error> {
1106 let mut exports = HashMap::with_capacity(ndts.len());
1107
1108 let ndts = ndts
1109 .filter(|ndt| ndt.ty.is_some())
1110 .map(|ndt| {
1111 let export_name = exported_type_name(exporter, ndt);
1112 if let Some(other) = exports.insert(export_name.to_string(), ndt.location) {
1113 return Err(Error::duplicate_type_name(export_name, ndt.location, other));
1114 }
1115
1116 Ok(ndt)
1117 })
1118 .collect::<Result<Vec<_>, _>>()?;
1119
1120 primitives::export_internal(s, exporter, format, types, ndts.into_iter(), indent)?;
1121
1122 Ok(exports)
1123}
1124
1125fn collect_existing_files(root: &Path) -> Result<HashSet<PathBuf>, Error> {
1127 if !root.exists() {
1128 return Ok(HashSet::new());
1129 }
1130
1131 let mut files = HashSet::new();
1132 let entries =
1133 std::fs::read_dir(root).map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
1134 for entry in entries {
1135 let entry = entry.map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
1136 let path = entry.path();
1137 let file_type = entry
1138 .file_type()
1139 .map_err(|source| Error::metadata(path.clone(), source))?;
1140
1141 if file_type.is_symlink() {
1142 continue;
1143 }
1144
1145 if file_type.is_dir() {
1146 files.extend(collect_existing_files(&path)?);
1147 } else if matches!(path.extension().and_then(|e| e.to_str()), Some("ts" | "js")) {
1148 files.insert(path);
1149 }
1150 }
1151
1152 Ok(files)
1153}
1154
1155fn is_generated_specta_file(path: &Path, exporter: &Exporter) -> Result<bool, Error> {
1156 match std::fs::read_to_string(path) {
1157 Ok(contents) => Ok((!exporter.framework_prelude.is_empty()
1158 && contents.contains(exporter.framework_prelude.as_ref()))
1159 || contents.contains("generated by Specta")),
1160 Err(err) if err.kind() == std::io::ErrorKind::InvalidData => Ok(false),
1161 Err(source) => Err(Error::read_file(path.to_path_buf(), source)),
1162 }
1163}
1164
1165fn remove_empty_dirs(path: &Path, root: &Path) -> Result<(), Error> {
1167 let entries =
1168 std::fs::read_dir(path).map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
1169 for entry in entries {
1170 let entry = entry.map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
1171 let entry_path = entry.path();
1172 let file_type = entry
1173 .file_type()
1174 .map_err(|source| Error::metadata(entry_path.clone(), source))?;
1175 if file_type.is_symlink() {
1176 continue;
1177 }
1178 if file_type.is_dir() {
1179 remove_empty_dirs(&entry_path, root)?;
1180 }
1181 }
1182
1183 let is_empty = path
1184 .read_dir()
1185 .map_err(|source| Error::read_dir(path.to_path_buf(), source))?
1186 .next()
1187 .is_none();
1188
1189 if path != root && is_empty {
1190 match std::fs::remove_dir(path) {
1191 Ok(()) => {}
1192 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1193 Err(source) => {
1194 return Err(Error::remove_dir(path.to_path_buf(), source));
1195 }
1196 }
1197 }
1198 Ok(())
1199}
1200
1201fn cleanup_stale_files(
1203 root: &Path,
1204 current_files: &HashMap<PathBuf, String>,
1205 exporter: &Exporter,
1206) -> Result<(), Error> {
1207 if !root.exists() {
1208 return Ok(());
1209 }
1210
1211 for path in collect_existing_files(root)? {
1212 if current_files.contains_key(&path) || !is_generated_specta_file(&path, exporter)? {
1213 continue;
1214 }
1215
1216 std::fs::remove_file(&path).or_else(|source| {
1217 if source.kind() == std::io::ErrorKind::NotFound {
1218 Ok(())
1219 } else {
1220 Err(Error::remove_file(path.clone(), source))
1221 }
1222 })?;
1223 }
1224
1225 remove_empty_dirs(root, root)?;
1226
1227 Ok(())
1228}
1229
1230fn exported_type_name(exporter: &Exporter, ndt: &NamedDataType) -> Cow<'static, str> {
1231 match exporter.layout {
1232 Layout::ModulePrefixedName => {
1233 let mut s = ndt.module_path.split("::").collect::<Vec<_>>().join("_");
1234 s.push('_');
1235 s.push_str(&ndt.name);
1236 Cow::Owned(s)
1237 }
1238 _ => ndt.name.clone(),
1239 }
1240}
1241
1242pub(crate) fn module_alias(module_path: &str) -> String {
1243 if module_path.is_empty() {
1244 "$root".to_string()
1245 } else {
1246 module_path.split("::").collect::<Vec<_>>().join("$")
1247 }
1248}
1249
1250fn module_import_statement(
1251 exporter: &Exporter,
1252 from_module_path: &str,
1253 to_module_path: &str,
1254) -> String {
1255 let import_keyword = if exporter.jsdoc {
1256 "import"
1257 } else {
1258 "import type"
1259 };
1260
1261 format!(
1262 "{} * as {} from \"{}\";",
1263 import_keyword,
1264 module_alias(to_module_path),
1265 module_import_path(from_module_path, to_module_path)
1266 )
1267}
1268
1269fn module_import_block(
1270 exporter: &Exporter,
1271 from_module_path: &str,
1272 import_paths: &BTreeSet<String>,
1273) -> String {
1274 if exporter.jsdoc {
1275 let mut out = String::from("/**\n");
1276
1277 for module_path in import_paths {
1278 out.push_str(" * @typedef {import(\"");
1279 out.push_str(&module_import_path(from_module_path, module_path));
1280 out.push_str("\")} ");
1281 out.push_str(&module_alias(module_path));
1282 out.push('\n');
1283 }
1284
1285 out.push_str(" */");
1286 out
1287 } else {
1288 import_paths
1289 .iter()
1290 .map(|module_path| module_import_statement(exporter, from_module_path, module_path))
1291 .collect::<Vec<_>>()
1292 .join("\n")
1293 }
1294}
1295
1296fn module_import_path(from_module_path: &str, to_module_path: &str) -> String {
1297 fn module_file_segments(module_path: &str) -> Vec<&str> {
1298 if module_path.is_empty() {
1299 vec!["index"]
1300 } else {
1301 module_path.split("::").collect()
1302 }
1303 }
1304
1305 let from_file_segments = module_file_segments(from_module_path);
1306 let from_dir_segments = &from_file_segments[..from_file_segments.len() - 1];
1307 let to_file_segments = module_file_segments(to_module_path);
1308
1309 let shared = from_dir_segments
1310 .iter()
1311 .zip(to_file_segments.iter())
1312 .take_while(|(a, b)| a == b)
1313 .count();
1314
1315 let mut relative_parts = Vec::new();
1316 relative_parts.extend(std::iter::repeat_n(
1317 "..",
1318 from_dir_segments.len().saturating_sub(shared),
1319 ));
1320 relative_parts.extend(to_file_segments.iter().skip(shared).copied());
1321
1322 if relative_parts
1323 .first()
1324 .is_none_or(|v| *v != "." && *v != "..")
1325 {
1326 relative_parts.insert(0, ".");
1327 }
1328
1329 relative_parts.join("/")
1330}