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