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// }