1use std::collections::HashMap;
7use std::fs::File;
8use std::io::{self, Write};
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::{env, fs};
12
13use anyhow::Context;
14use serde::{Deserialize, Serialize};
15
16#[derive(Deserialize, Serialize)]
18pub struct RiptDotToml {
19 #[serde(default = "default_outfile")]
21 outfile: PathBuf,
22
23 #[serde(default = "default_camelize_namespaces")]
27 camelize_namespaces: bool,
28
29 #[serde(default = "default_overrides")]
34 overrides: HashMap<String, TsKeywordType>,
35
36 #[serde(default = "default_wrapper_package_import")]
39 wrapper_package_import: String,
40}
41
42impl FromStr for RiptDotToml {
43 type Err = toml::de::Error;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 toml::from_str(s)
47 }
48}
49
50fn default_outfile() -> PathBuf {
51 PathBuf::from("target/inertia.ts")
52}
53
54fn default_camelize_namespaces() -> bool {
55 true
56}
57
58fn default_overrides() -> HashMap<String, TsKeywordType> {
59 HashMap::new()
60}
61
62fn default_wrapper_package_import() -> String {
63 "ript".to_string()
64}
65
66impl Default for RiptDotToml {
67 fn default() -> Self {
68 Self {
69 outfile: default_outfile(),
70 camelize_namespaces: default_camelize_namespaces(),
71 overrides: default_overrides(),
72 wrapper_package_import: default_wrapper_package_import(),
73 }
74 }
75}
76
77impl RiptDotToml {
78 pub fn provision() -> anyhow::Result<PathBuf> {
86 let path = ript_dot_doml()?;
87
88 if !path.exists() {
89 std::fs::write(&path, toml::to_string(&Self::default())?)?;
90 }
91
92 Ok(path)
93 }
94
95 pub fn outfile_writer(&self) -> anyhow::Result<TsWriter> {
97 TsWriter::new(&self.outfile)
98 }
99
100 pub fn outfile(&self) -> &PathBuf {
105 &self.outfile
106 }
107
108 pub fn camelize_namespaces(&self) -> bool {
115 self.camelize_namespaces
116 }
117
118 pub fn overrides(&self) -> &HashMap<String, TsKeywordType> {
119 &self.overrides
120 }
121
122 pub fn wrapper_package_import(&self) -> &str {
123 &self.wrapper_package_import
124 }
125}
126
127fn ript_dot_doml() -> anyhow::Result<PathBuf> {
128 let workspace_root = detect_workspace_root()?;
129 Ok(workspace_root.join("ript.toml"))
130}
131
132pub fn detect_workspace_root() -> anyhow::Result<PathBuf> {
135 let mut current_dir =
136 env::current_dir().context("failed to determine the current working directory")?;
137
138 let mut last_cargo_toml: Option<PathBuf> = None;
139
140 loop {
141 let cargo_toml_path = current_dir.join("Cargo.toml");
142 if cargo_toml_path.exists() {
143 last_cargo_toml = Some(cargo_toml_path.clone());
145
146 if let Ok(contents) = fs::read_to_string(&cargo_toml_path) {
148 if contents
150 .lines()
151 .any(|line| line.trim_start().starts_with("[workspace]"))
152 {
153 return Ok(current_dir);
154 }
155 }
156 }
157
158 if !current_dir.pop() {
160 break;
161 }
162 }
163
164 if let Some(path) = last_cargo_toml {
166 return Ok(path.parent().context("no parent")?.to_path_buf());
167 }
168
169 anyhow::bail!("could not find a Cargo.toml in any parent directories")
173}
174
175pub struct TsWriter {
177 file: File,
178}
179
180impl TsWriter {
181 fn new(path: &PathBuf) -> anyhow::Result<Self> {
183 if let Some(parent_dir) = path.parent() {
185 fs::create_dir_all(parent_dir)?;
186 }
187
188 Ok(Self {
189 file: File::create(path)?,
190 })
191 }
192}
193
194impl Write for TsWriter {
195 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
196 self.file.write(buf)
197 }
198
199 fn flush(&mut self) -> io::Result<()> {
200 self.file.flush()
201 }
202}
203
204#[derive(Deserialize, Serialize, Clone, Copy)]
206#[serde(rename_all = "camelCase")]
207pub enum TsKeywordType {
208 String,
210 Number,
212}