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)]
38 omit_crate_name: bool,
39
40 #[serde(default = "default_ript_preamble_namespace")]
41 ript_preamble_namespace: String,
42}
43
44impl FromStr for RiptDotToml {
45 type Err = toml::de::Error;
46
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 toml::from_str(s)
49 }
50}
51
52fn default_outfile() -> PathBuf {
53 PathBuf::from("target/inertia.ts")
54}
55
56fn default_camelize_namespaces() -> bool {
57 true
58}
59
60fn default_overrides() -> HashMap<String, TsKeywordType> {
61 HashMap::new()
62}
63
64fn default_ript_preamble_namespace() -> String {
65 "ript::ript".to_string()
66}
67
68impl Default for RiptDotToml {
69 fn default() -> Self {
70 Self {
71 outfile: default_outfile(),
72 camelize_namespaces: default_camelize_namespaces(),
73 overrides: default_overrides(),
74 omit_crate_name: Default::default(),
75 ript_preamble_namespace: default_ript_preamble_namespace(),
76 }
77 }
78}
79
80impl RiptDotToml {
81 pub fn provision() -> anyhow::Result<PathBuf> {
89 let path = ript_dot_doml()?;
90
91 if !path.exists() {
92 std::fs::write(&path, toml::to_string(&Self::default())?)?;
93 }
94
95 Ok(path)
96 }
97
98 pub fn ript_preamble_namespace(&self) -> &str {
99 &self.ript_preamble_namespace
100 }
101
102 pub fn outfile_writer(&self) -> anyhow::Result<TsWriter> {
104 TsWriter::new(&self.outfile)
105 }
106
107 pub fn outfile(&self) -> &PathBuf {
112 &self.outfile
113 }
114
115 pub fn camelize_namespaces(&self) -> bool {
122 self.camelize_namespaces
123 }
124
125 pub fn overrides(&self) -> &HashMap<String, TsKeywordType> {
126 &self.overrides
127 }
128
129 pub fn omit_crate_name(&self) -> bool {
131 self.omit_crate_name
132 }
133}
134
135fn ript_dot_doml() -> anyhow::Result<PathBuf> {
136 let workspace_root = detect_workspace_root()?;
137 Ok(workspace_root.join("ript.toml"))
138}
139
140pub fn detect_workspace_root() -> anyhow::Result<PathBuf> {
143 let mut current_dir =
144 env::current_dir().context("failed to determine the current working directory")?;
145
146 let mut last_cargo_toml: Option<PathBuf> = None;
147
148 loop {
149 let cargo_toml_path = current_dir.join("Cargo.toml");
150 if cargo_toml_path.exists() {
151 last_cargo_toml = Some(cargo_toml_path.clone());
153
154 if let Ok(contents) = fs::read_to_string(&cargo_toml_path) {
156 if contents
158 .lines()
159 .any(|line| line.trim_start().starts_with("[workspace]"))
160 {
161 return Ok(current_dir);
162 }
163 }
164 }
165
166 if !current_dir.pop() {
168 break;
169 }
170 }
171
172 if let Some(path) = last_cargo_toml {
174 return Ok(path.parent().context("no parent")?.to_path_buf());
175 }
176
177 anyhow::bail!("could not find a Cargo.toml in any parent directories")
181}
182
183pub struct TsWriter {
185 file: File,
186}
187
188impl TsWriter {
189 fn new(path: &PathBuf) -> anyhow::Result<Self> {
191 if let Some(parent_dir) = path.parent() {
193 fs::create_dir_all(parent_dir)?;
194 }
195
196 Ok(Self {
197 file: File::create(path)?,
198 })
199 }
200}
201
202impl Write for TsWriter {
203 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
204 self.file.write(buf)
205 }
206
207 fn flush(&mut self) -> io::Result<()> {
208 self.file.flush()
209 }
210}
211
212#[derive(Deserialize, Serialize, Clone, Copy)]
214#[serde(rename_all = "camelCase")]
215pub enum TsKeywordType {
216 String,
218 Number,
220}