typeshare_engine/config.rs
1use std::{
2 collections::BTreeMap,
3 env, fs,
4 path::{Path, PathBuf},
5};
6
7use anyhow::Context;
8use serde::{ser, Deserialize, Serialize};
9use typeshare_model::Language;
10
11use crate::serde::{args::ArgsSetSerializer, config::ConfigDeserializer, empty::EmptyDeserializer};
12
13pub use crate::serde::args::CliArgsSet;
14
15const DEFAULT_CONFIG_FILE_NAME: &str = "typeshare.toml";
16
17#[derive(Debug, Clone, Default, Deserialize)]
18pub struct GlobalConfig {
19 /// If present, only fields / variants / items that are accepted by at
20 /// least one of these os's will be emitted.
21 #[serde(default)]
22 pub target_os: Option<Vec<String>>,
23}
24
25/// A partially parsed typeshare config file.
26///
27/// This contains a `toml::Table` for each language that was found in the config
28/// file. The `Config` type on the `Language` trait can be further deserialized
29/// from this toml table. It also contains config that's specific to typeshare
30/// itself.
31#[derive(Debug, Clone, Default, Deserialize)]
32pub struct Config {
33 /// toml::Table doesn't have a const constructor, so there's not an easy
34 /// way to make a long-lived empty table to deserialize from when the
35 /// language is absent from the raw data. So we just put one here.
36 ///
37 /// This should literally always be empty.
38 #[serde(skip)]
39 empty: toml::Table,
40
41 /// When we load the typeshare config file, we don't know precisely which
42 /// languages we're going to have yet. So we parse them into arbitrary
43 /// toml, keyed by language, which we will later deserialize into a
44 /// specific language's config type.
45 #[serde(flatten)]
46 raw_data: BTreeMap<String, toml::Table>,
47
48 // General config for typeshare, separate from any particular language
49 #[serde(default)]
50 typeshare: GlobalConfig,
51}
52
53impl Config {
54 /// Retrieve the config for the given language, by deserializing it into
55 /// the given type. The deserialize implementation should be able to handle
56 /// arbitrary missing keys by populating them with default values. Errors
57 ///
58 pub fn config_for_language(&self, language: &str) -> &toml::Table {
59 self.raw_data.get(language).unwrap_or(&self.empty)
60 }
61
62 // Store a config for a language, overriding the existing one, by
63 // serializing the config type into a toml table
64 pub fn store_config_for_language<T: Serialize>(
65 &mut self,
66 language: &str,
67 config: &T,
68 ) -> anyhow::Result<()> {
69 todo!()
70 // self.raw_data.insert(
71 // language.to_owned(),
72 // config
73 // .serialize(TableSerializer)
74 // .context("error converting config to toml")?,
75 // );
76
77 // Ok(())
78 }
79
80 pub fn global_config(&self) -> &GlobalConfig {
81 &self.typeshare
82 }
83}
84
85impl Serialize for Config {
86 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87 where
88 S: ser::Serializer,
89 {
90 self.raw_data.serialize(serializer)
91 }
92}
93
94pub fn compute_args_set<'a, L: Language<'a>>() -> anyhow::Result<CliArgsSet> {
95 let empty_config = L::Config::deserialize(EmptyDeserializer).context(
96 "failed to create empty config; \
97 did you forget `#[serde(default)]`?",
98 )?;
99
100 let args_set = empty_config
101 .serialize(ArgsSetSerializer::new(L::NAME))
102 .context("failed to compute CLI arguments from language configuration type")?;
103
104 Ok(args_set)
105}
106
107// pub fn store_config(config: &Config, file_path: Option<&str>) -> anyhow::Result<()> {
108// let file_path = file_path.unwrap_or(DEFAULT_CONFIG_FILE_NAME);
109// let config_output = toml::to_string_pretty(config).context("Failed to serialize to toml")?;
110
111// // Fail if trying to overwrite an existing config file
112// let mut file = OpenOptions::new()
113// .write(true)
114// .create_new(true)
115// .open(file_path)?;
116
117// file.write_all(config_output.as_bytes())?;
118
119// Ok(())
120// }
121
122pub fn load_config(file_path: Option<&Path>) -> anyhow::Result<Config> {
123 let file_path_buf;
124
125 let file_path = match file_path {
126 Some(path) => path,
127 None => match find_configuration_file() {
128 None => return Ok(Config::default()),
129 Some(path) => {
130 file_path_buf = path;
131 &file_path_buf
132 }
133 },
134 };
135
136 let config_string = fs::read_to_string(file_path).with_context(|| {
137 format!(
138 "i/o error reading typeshare config from '{path}'",
139 path = file_path.display()
140 )
141 })?;
142
143 toml::from_str(&config_string).with_context(|| {
144 format!(
145 "error loading typeshare config from '{path}'",
146 path = file_path.display()
147 )
148 })
149}
150
151/// Search each ancestor directory for configuration file
152fn find_configuration_file() -> Option<PathBuf> {
153 let mut path = env::current_dir().ok()?;
154 let file = Path::new(DEFAULT_CONFIG_FILE_NAME);
155
156 loop {
157 path.push(file);
158
159 if path.is_file() {
160 break Some(path);
161 } else if !(path.pop() && path.pop()) {
162 break None;
163 }
164 }
165}
166
167pub fn load_language_config<'a, 'config, L: Language<'config>>(
168 config_file_entry: &'config toml::Table,
169 cli_matches: &'config clap::ArgMatches,
170 spec: &'a CliArgsSet,
171) -> anyhow::Result<L::Config> {
172 L::Config::deserialize(ConfigDeserializer::new(
173 &config_file_entry,
174 &cli_matches,
175 spec,
176 ))
177 .context("error deserializing config")
178}
179
180pub fn load_language_config_from_file_and_args<'a, 'config, L: Language<'config>>(
181 config: &'config Config,
182 cli_matches: &'config clap::ArgMatches,
183 spec: &'a CliArgsSet,
184) -> anyhow::Result<L::Config> {
185 load_language_config::<L>(config.config_for_language(L::NAME), cli_matches, spec)
186}
187
188// #[cfg(test)]
189// mod test {
190// use super::*;
191
192// const CURRENT_DIR: &str = env!("CARGO_MANIFEST_DIR");
193// const TEST_DIR: &str = "data/tests";
194
195// fn config_file_path(filename: &str) -> PathBuf {
196// [CURRENT_DIR, TEST_DIR, filename].iter().collect()
197// }
198
199// #[test]
200// fn default_test() {
201// let path = config_file_path("default_config.toml");
202// let config = load_config(Some(path)).unwrap();
203
204// assert_eq!(config, Config::default());
205// }
206
207// #[test]
208// fn empty_test() {
209// let path = config_file_path("empty_config.toml");
210// let config = load_config(Some(path)).unwrap();
211
212// assert_eq!(config, Config::default());
213// }
214
215// #[test]
216// fn mappings_test() {
217// let path = config_file_path("mappings_config.toml");
218// let config = load_config(Some(path)).unwrap();
219
220// assert_eq!(config.swift.type_mappings["DateTime"], "Date");
221// assert_eq!(config.kotlin.type_mappings["DateTime"], "String");
222// assert_eq!(config.scala.type_mappings["DateTime"], "String");
223// assert_eq!(config.typescript.type_mappings["DateTime"], "string");
224// #[cfg(feature = "go")]
225// assert_eq!(config.go.type_mappings["DateTime"], "string");
226// }
227
228// #[test]
229// fn decorators_test() {
230// let path = config_file_path("decorators_config.toml");
231// let config = load_config(Some(path)).unwrap();
232
233// assert_eq!(config.swift.default_decorators.len(), 1);
234// assert_eq!(config.swift.default_decorators[0], "Sendable");
235// }
236
237// #[test]
238// fn constraints_test() {
239// let path = config_file_path("constraints_config.toml");
240// let config = load_config(Some(path)).unwrap();
241
242// assert_eq!(config.swift.default_generic_constraints.len(), 1);
243// assert_eq!(config.swift.default_generic_constraints[0], "Sendable");
244// }
245
246// #[test]
247// fn swift_prefix_test() {
248// let path = config_file_path("swift_prefix_config.toml");
249// let config = load_config(Some(path)).unwrap();
250
251// assert_eq!(config.swift.prefix, "test");
252// }
253// }