ts_rs_json_value/
export.rs

1use std::{
2    any::TypeId,
3    collections::BTreeMap,
4    fmt::Write,
5    path::{Component, Path, PathBuf},
6};
7
8use thiserror::Error;
9use ExportError::*;
10
11use crate::TS;
12
13const NOTE: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n";
14
15/// An error which may occur when exporting a type
16#[derive(Error, Debug)]
17pub enum ExportError {
18    #[error("this type cannot be exported")]
19    CannotBeExported,
20    #[cfg(feature = "format")]
21    #[error("an error occurred while formatting the generated typescript output")]
22    Formatting(String),
23    #[error("an error occurred while performing IO")]
24    Io(#[from] std::io::Error),
25    #[error("the environment variable CARGO_MANIFEST_DIR is not set")]
26    ManifestDirNotSet,
27}
28
29/// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute
30pub(crate) fn export_type<T: TS + ?Sized + 'static>() -> Result<(), ExportError> {
31    let path = output_path::<T>()?;
32    export_type_to::<T, _>(&path)
33}
34
35/// Export `T` to the file specified by the `path` argument.
36pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
37    path: P,
38) -> Result<(), ExportError> {
39    #[allow(unused_mut)]
40    let mut buffer = export_type_to_string::<T>()?;
41
42    // format output
43    #[cfg(feature = "format")]
44    {
45        use dprint_plugin_typescript::{configuration::ConfigurationBuilder, format_text};
46
47        let fmt_cfg = ConfigurationBuilder::new().deno().build();
48        if let Some(formatted) =
49            format_text(path.as_ref(), &buffer, &fmt_cfg).map_err(|e| Formatting(e.to_string()))?
50        {
51            buffer = formatted;
52        }
53    }
54
55    if let Some(parent) = path.as_ref().parent() {
56        std::fs::create_dir_all(parent)?;
57    }
58    std::fs::write(path.as_ref(), buffer)?;
59    Ok(())
60}
61
62/// Returns the generated defintion for `T`.
63pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
64    let mut buffer = String::with_capacity(1024);
65    buffer.push_str(NOTE);
66    generate_imports::<T>(&mut buffer)?;
67    generate_decl::<T>(&mut buffer);
68    Ok(buffer)
69}
70
71/// Compute the output path to where `T` should be exported.
72fn output_path<T: TS + ?Sized>() -> Result<PathBuf, ExportError> {
73    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| ManifestDirNotSet)?;
74    let manifest_dir = Path::new(&manifest_dir);
75    let path = PathBuf::from(T::EXPORT_TO.ok_or(CannotBeExported)?);
76    Ok(manifest_dir.join(path))
77}
78
79/// Push the declaration of `T`
80fn generate_decl<T: TS + ?Sized>(out: &mut String) {
81    out.push_str("export ");
82    out.push_str(&T::decl());
83}
84
85/// Push an import statement for all dependencies of `T`
86fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), ExportError> {
87    let path = Path::new(T::EXPORT_TO.ok_or(ExportError::CannotBeExported)?);
88
89    let deps = T::dependencies();
90    let deduplicated_deps = deps
91        .iter()
92        .filter(|dep| dep.type_id != TypeId::of::<T>())
93        .map(|dep| (&dep.ts_name, dep))
94        .collect::<BTreeMap<_, _>>();
95
96    for (_, dep) in deduplicated_deps {
97        let rel_path = import_path(path, Path::new(dep.exported_to));
98        writeln!(
99            out,
100            "import type {{ {} }} from {:?};",
101            &dep.ts_name, rel_path
102        )
103        .unwrap();
104    }
105    writeln!(out).unwrap();
106    Ok(())
107}
108
109/// Returns the required import path for importing `import` from the file `from`
110fn import_path(from: &Path, import: &Path) -> String {
111    let rel_path =
112        diff_paths(import, from.parent().unwrap()).expect("failed to calculate import path");
113    match rel_path.components().next() {
114        Some(Component::Normal(_)) => format!("./{}", rel_path.to_string_lossy()),
115        _ => rel_path.to_string_lossy().into(),
116    }
117    .trim_end_matches(".ts")
118    .to_owned()
119}
120
121// Construct a relative path from a provided base directory path to the provided path.
122//
123// Copyright 2012-2015 The Rust Project Developers.
124//
125// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
126// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
127// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
128// option. This file may not be copied, modified, or distributed
129// except according to those terms.
130//
131// Adapted from rustc's path_relative_from
132// https://github.com/rust-lang/rust/blob/e1d0de82cc40b666b88d4a6d2c9dcbc81d7ed27f/src/librustc_back/rpath.rs#L116-L158
133fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
134where
135    P: AsRef<Path>,
136    B: AsRef<Path>,
137{
138    let path = path.as_ref();
139    let base = base.as_ref();
140
141    if path.is_absolute() != base.is_absolute() {
142        if path.is_absolute() {
143            Some(PathBuf::from(path))
144        } else {
145            None
146        }
147    } else {
148        let mut ita = path.components();
149        let mut itb = base.components();
150        let mut comps: Vec<Component> = vec![];
151        loop {
152            match (ita.next(), itb.next()) {
153                (None, None) => break,
154                (Some(a), None) => {
155                    comps.push(a);
156                    comps.extend(ita.by_ref());
157                    break;
158                }
159                (None, _) => comps.push(Component::ParentDir),
160                (Some(a), Some(b)) if comps.is_empty() && a == b => (),
161                (Some(a), Some(Component::CurDir)) => comps.push(a),
162                (Some(_), Some(Component::ParentDir)) => return None,
163                (Some(a), Some(_)) => {
164                    comps.push(Component::ParentDir);
165                    for _ in itb {
166                        comps.push(Component::ParentDir);
167                    }
168                    comps.push(a);
169                    comps.extend(ita.by_ref());
170                    break;
171                }
172            }
173        }
174        Some(comps.iter().map(|c| c.as_os_str()).collect())
175    }
176}