Skip to main content

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