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