From 2e31ce9debece7c6a013ab5bd8c012afa948ca04 Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Tue, 13 Jun 2023 16:59:34 +0200 Subject: [PATCH 1/8] Read configuration file from PWD/.topiary/languages.toml The content of this file is then merged with the default one --- Cargo.lock | 10 ++++++++++ Cargo.toml | 1 + topiary-cli/Cargo.toml | 1 + topiary-cli/src/configuration.rs | 29 +++++++++++++++++++++++++++++ topiary-cli/src/main.rs | 14 ++++++++++++-- topiary-playground/build.rs | 2 +- topiary-playground/src/lib.rs | 2 +- topiary/benches/benchmark.rs | 2 +- topiary/src/configuration.rs | 25 ++++++++++++++++--------- topiary/src/error.rs | 10 ++++++++++ topiary/src/lib.rs | 6 +++--- topiary/tests/sample-tester.rs | 6 +++--- 12 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 topiary-cli/src/configuration.rs diff --git a/Cargo.lock b/Cargo.lock index 5a3c3f13..17576db6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-toml-merge" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78072b550e5c20bc4a9d1384be28809cbdb7b25b2b4707ddc6d908b7e6de3bf" +dependencies = [ + "toml", +] + [[package]] name = "serde_derive" version = "1.0.164" @@ -1397,6 +1406,7 @@ dependencies = [ "clap 4.3.3", "env_logger", "log", + "serde-toml-merge", "tempfile", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index 4132ad90..00383623 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ pretty_assertions = "1.3" prettydiff = "0.6.4" regex = "1.8.2" serde = "1.0.163" +serde-toml-merge = "0.3" serde_json = "1.0" tempfile = "3.5.0" test-log = "0.2.11" diff --git a/topiary-cli/Cargo.toml b/topiary-cli/Cargo.toml index c8502a2a..30c79406 100644 --- a/topiary-cli/Cargo.toml +++ b/topiary-cli/Cargo.toml @@ -29,6 +29,7 @@ path = "src/main.rs" clap = { workspace = true, features = ["derive"] } env_logger = { workspace = true } log = { workspace = true } +serde-toml-merge = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } toml = { workspace = true } diff --git a/topiary-cli/src/configuration.rs b/topiary-cli/src/configuration.rs new file mode 100644 index 00000000..3a419713 --- /dev/null +++ b/topiary-cli/src/configuration.rs @@ -0,0 +1,29 @@ +use serde_toml_merge::merge; +use std::env::current_dir; +use topiary::{default_configuration_toml, Configuration}; + +pub fn parse_configuration() -> Configuration { + user_lang_toml() + .expect("TODO: Error") + .try_into() + .expect("TODO: Error") +} + +/// User configured languages.toml file, merged with the default config. +fn user_lang_toml() -> Result { + let config = [current_dir().unwrap().join(".topiary")] + .into_iter() + .map(|path| path.join("languages.toml")) + .filter_map(|file| { + std::fs::read_to_string(file) + .map(|config| toml::from_str(&config)) + .ok() + }) + .collect::, _>>()? + .into_iter() + .fold(default_configuration_toml(), |a, b| { + merge(a, b).expect("TODO: Gracefull fail") + }); + + Ok(config) +} diff --git a/topiary-cli/src/main.rs b/topiary-cli/src/main.rs index 07830019..169eadd5 100644 --- a/topiary-cli/src/main.rs +++ b/topiary-cli/src/main.rs @@ -1,8 +1,10 @@ +mod configuration; mod error; mod output; mod visualise; use std::{ + eprintln, error::Error, fs::File, io::{stdin, BufReader, BufWriter, Read}, @@ -11,13 +13,14 @@ use std::{ }; use clap::{ArgGroup, Parser}; +use configuration::parse_configuration; use crate::{ error::{CLIError, CLIResult, TopiaryError}, output::OutputFile, visualise::Visualisation, }; -use topiary::{formatter, Configuration, Language, Operation, SupportedLanguage}; +use topiary::{formatter, Language, Operation, SupportedLanguage}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -64,6 +67,9 @@ struct Args { /// Do not check that formatting twice gives the same output #[arg(short, long, display_order = 7)] skip_idempotence: bool, + + #[arg(long, display_order = 8)] + output_configuration: bool, } #[tokio::main] @@ -80,7 +86,11 @@ async fn run() -> CLIResult<()> { env_logger::init(); let args = Args::parse(); - let configuration = Configuration::parse_default_config(); + let configuration = parse_configuration(); + + if args.output_configuration { + eprintln!("{:#?}", configuration); + } // The as_deref() gives us an Option<&str>, which we can match against // string literals diff --git a/topiary-playground/build.rs b/topiary-playground/build.rs index e906dc92..44227c2a 100644 --- a/topiary-playground/build.rs +++ b/topiary-playground/build.rs @@ -51,7 +51,7 @@ fn main() { for path in input_files { let path = path.unwrap().path(); if let Some(ext) = path.extension().map(|ext| ext.to_string_lossy()) { - if !Configuration::parse_default_config() + if !Configuration::parse_default_configuration() .known_extensions() .contains(&*ext) || ext == "mli" diff --git a/topiary-playground/src/lib.rs b/topiary-playground/src/lib.rs index d82b096f..fcf41ad5 100644 --- a/topiary-playground/src/lib.rs +++ b/topiary-playground/src/lib.rs @@ -45,7 +45,7 @@ async fn format_inner( ) -> FormatterResult { let mut output = Vec::new(); - let configuration = Configuration::parse_default_config(); + let configuration = Configuration::parse_default_configuration(); let language = configuration.get_language(language_name)?; let grammars = language.grammars_wasm().await?; diff --git a/topiary/benches/benchmark.rs b/topiary/benches/benchmark.rs index e0fcea3b..4df651b8 100644 --- a/topiary/benches/benchmark.rs +++ b/topiary/benches/benchmark.rs @@ -8,7 +8,7 @@ use topiary::{formatter, Operation}; async fn format() { let input = fs::read_to_string("tests/samples/input/ocaml.ml").unwrap(); let query = fs::read_to_string("../languages/ocaml.scm").unwrap(); - let configuration = Configuration::parse_default_config(); + let configuration = Configuration::parse_default_configuration(); let language = configuration.get_language("ocaml_implementation").unwrap(); let grammars = language.grammars().await.unwrap(); diff --git a/topiary/src/configuration.rs b/topiary/src/configuration.rs index c202e0c4..8b7744d1 100644 --- a/topiary/src/configuration.rs +++ b/topiary/src/configuration.rs @@ -3,20 +3,14 @@ use std::{collections::HashSet, str::from_utf8}; use crate::{language::Language, FormatterError, FormatterResult}; use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct Configuration { pub language: Vec, } impl Configuration { - // TODO: Should be able to take a filepath. - // TODO: Should return a FormatterResult rather than panicking. - #[must_use] - pub fn parse_default_config() -> Self { - let default_config = include_bytes!("../../languages.toml"); - let default_config = toml::from_str(from_utf8(default_config).unwrap()) - .expect("Could not parse built-in languages.toml"); - default_config + pub fn new() -> Self { + Configuration { language: vec![] } } #[must_use] @@ -40,4 +34,17 @@ impl Configuration { name.as_ref().to_string(), )); } + + pub fn parse_default_configuration() -> Self { + default_configuration_toml() + .try_into() + .expect("TODO: Error") + } +} + +/// Default built-in languages.toml. +pub fn default_configuration_toml() -> toml::Value { + let default_config = include_bytes!("../../languages.toml"); + toml::from_str(from_utf8(default_config).unwrap()) + .expect("Could not parse built-in languages.toml to valid toml") } diff --git a/topiary/src/error.rs b/topiary/src/error.rs index e5b43437..47638e21 100644 --- a/topiary/src/error.rs +++ b/topiary/src/error.rs @@ -40,6 +40,9 @@ pub enum FormatterError { /// The configuration file or command line mentions an unsupported language UnsupportedLanguage(String), + + /// Configuration file parsing error + ConfigurationParsing(PathBuf, toml::de::Error), } /// A subtype of `FormatterError::Io` @@ -110,6 +113,12 @@ impl fmt::Display for FormatterError { Self::UnsupportedLanguage(language) => { write!(f, "The following language is not supported: {language}") } + + Self::ConfigurationParsing(filepath, err) => { + let filepath = filepath.to_string_lossy(); + let message = err.message(); + write!(f, "Could not parse the configuration file at {filepath}. Parsing returned the following error: {message}") + } } } } @@ -128,6 +137,7 @@ impl Error for FormatterError { Self::Io(IoError::Filesystem(_, source)) => Some(source), Self::Io(IoError::Generic(_, Some(source))) => Some(source.as_ref()), Self::Formatting(err) => Some(err), + Self::ConfigurationParsing(_, err) => Some(err), } } } diff --git a/topiary/src/lib.rs b/topiary/src/lib.rs index 82b4e08c..efd12d8f 100644 --- a/topiary/src/lib.rs +++ b/topiary/src/lib.rs @@ -16,7 +16,7 @@ use itertools::Itertools; use pretty_assertions::StrComparison; pub use crate::{ - configuration::Configuration, + configuration::{default_configuration_toml, Configuration}, error::{FormatterError, IoError}, language::{Language, SupportedLanguage}, tree_sitter::{apply_query, SyntaxNode, Visualisation}, @@ -134,7 +134,7 @@ pub enum Operation { /// let mut query = String::new(); /// query_file.read_to_string(&mut query).expect("read query file"); /// -/// let config = Configuration::parse_default_config(); +/// let config = Configuration::parse_default_configuration(); /// let language = config.get_language("json").unwrap(); /// let grammars = language /// .grammars() @@ -276,7 +276,7 @@ mod test { let mut input = "[ 1, % ]".as_bytes(); let mut output = Vec::new(); let query = "(#language! json)"; - let configuration = Configuration::parse_default_config(); + let configuration = Configuration::parse_default_configuration(); let language = configuration.get_language("json").unwrap(); let grammars = language.grammars().await.unwrap(); diff --git a/topiary/tests/sample-tester.rs b/topiary/tests/sample-tester.rs index e649990e..d881e0e6 100644 --- a/topiary/tests/sample-tester.rs +++ b/topiary/tests/sample-tester.rs @@ -27,7 +27,7 @@ fn pretty_assert_eq(v1: &str, v2: &str) { async fn input_output_tester() { let input_dir = fs::read_dir("tests/samples/input").unwrap(); let expected_dir = Path::new("tests/samples/expected"); - let config = Configuration::parse_default_config(); + let config = Configuration::parse_default_configuration(); let extensions = config.known_extensions(); for file in input_dir { @@ -71,7 +71,7 @@ async fn input_output_tester() { // Test that our query files are properly formatted #[test(tokio::test)] async fn formatted_query_tester() { - let config = Configuration::parse_default_config(); + let config = Configuration::parse_default_configuration(); let language_dir = fs::read_dir("../languages").unwrap(); for file in language_dir { @@ -108,7 +108,7 @@ async fn formatted_query_tester() { // Test that all queries are used on sample files #[test(tokio::test)] async fn exhaustive_query_tester() { - let config = Configuration::parse_default_config(); + let config = Configuration::parse_default_configuration(); let input_dir = fs::read_dir("tests/samples/input").unwrap(); for file in input_dir { From d24e2d9100750628d8a6956c91e96a27ad9b4e10 Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 09:08:36 +0200 Subject: [PATCH 2/8] Implement Default trait for Configuration --- topiary/src/configuration.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/topiary/src/configuration.rs b/topiary/src/configuration.rs index 8b7744d1..d11cf3c0 100644 --- a/topiary/src/configuration.rs +++ b/topiary/src/configuration.rs @@ -42,6 +42,12 @@ impl Configuration { } } +impl Default for Configuration { + fn default() -> Self { + Self::new() + } +} + /// Default built-in languages.toml. pub fn default_configuration_toml() -> toml::Value { let default_config = include_bytes!("../../languages.toml"); From 51b6d88cc08caf08e62e47567212a1262ee430e9 Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 10:35:24 +0200 Subject: [PATCH 3/8] Update usage with --output-configuration option --- README.md | 1 + topiary-cli/src/main.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index ec796359..649842d3 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ Options: -i, --in-place Format the input file in place -v, --visualise[=] Visualise the syntax tree, rather than format [possible values: json, dot] -s, --skip-idempotence Do not check that formatting twice gives the same output + --output-configuration Output the full configuration to stderr before continuing -h, --help Print help -V, --version Print version ``` diff --git a/topiary-cli/src/main.rs b/topiary-cli/src/main.rs index 169eadd5..bdc1873b 100644 --- a/topiary-cli/src/main.rs +++ b/topiary-cli/src/main.rs @@ -68,6 +68,7 @@ struct Args { #[arg(short, long, display_order = 7)] skip_idempotence: bool, + /// Output the full configuration to stderr before continuing #[arg(long, display_order = 8)] output_configuration: bool, } From 7a114cb32abc0d9ffb4da945eb9cc46a7c636b01 Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 12:28:15 +0200 Subject: [PATCH 4/8] Look up from $PWD to find the directory containing .topiary --- topiary-cli/src/configuration.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/topiary-cli/src/configuration.rs b/topiary-cli/src/configuration.rs index 3a419713..b53371df 100644 --- a/topiary-cli/src/configuration.rs +++ b/topiary-cli/src/configuration.rs @@ -1,17 +1,18 @@ use serde_toml_merge::merge; -use std::env::current_dir; +use std::{env::current_dir, path::PathBuf}; use topiary::{default_configuration_toml, Configuration}; pub fn parse_configuration() -> Configuration { - user_lang_toml() + user_configuration_toml() .expect("TODO: Error") .try_into() .expect("TODO: Error") } /// User configured languages.toml file, merged with the default config. -fn user_lang_toml() -> Result { - let config = [current_dir().unwrap().join(".topiary")] +fn user_configuration_toml() -> Result { + // TODO(Erin): Error on failure to find workspace + let config = [find_workspace().join(".topiary")] .into_iter() .map(|path| path.join("languages.toml")) .filter_map(|file| { @@ -27,3 +28,16 @@ fn user_lang_toml() -> Result { Ok(config) } + +pub fn find_workspace() -> PathBuf { + let current_dir = current_dir().expect("Could not get current working directory"); + for ancestor in current_dir.ancestors() { + if ancestor.join(".topiary").exists() { + return ancestor.to_owned(); + } + } + + // Default to the current dir if we could not find an ancestor with the .topiary directory + // If current_dir does not contain a .topiary, it will be filtered our in the `user_lang_toml` function. + current_dir +} From 994044b0cd2be4e5e086d9aa0a607181e14f5978 Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 13:00:36 +0200 Subject: [PATCH 5/8] Implement from for toml Error to TopiaryError --- topiary-cli/src/configuration.rs | 12 ++++++------ topiary-cli/src/error.rs | 9 +++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/topiary-cli/src/configuration.rs b/topiary-cli/src/configuration.rs index b53371df..377d45a7 100644 --- a/topiary-cli/src/configuration.rs +++ b/topiary-cli/src/configuration.rs @@ -2,16 +2,16 @@ use serde_toml_merge::merge; use std::{env::current_dir, path::PathBuf}; use topiary::{default_configuration_toml, Configuration}; -pub fn parse_configuration() -> Configuration { - user_configuration_toml() - .expect("TODO: Error") +use crate::error::{CLIError, CLIResult, TopiaryError}; + +pub fn parse_configuration() -> CLIResult { + user_configuration_toml()? .try_into() - .expect("TODO: Error") + .map_err(TopiaryError::from) } /// User configured languages.toml file, merged with the default config. -fn user_configuration_toml() -> Result { - // TODO(Erin): Error on failure to find workspace +fn user_configuration_toml() -> CLIResult { let config = [find_workspace().join(".topiary")] .into_iter() .map(|path| path.join("languages.toml")) diff --git a/topiary-cli/src/error.rs b/topiary-cli/src/error.rs index f718a504..4d5acbf8 100644 --- a/topiary-cli/src/error.rs +++ b/topiary-cli/src/error.rs @@ -116,3 +116,12 @@ where ) } } + +impl From for TopiaryError { + fn from(e: toml::de::Error) -> Self { + TopiaryError::Bin( + "Could not parse user configuration".to_owned(), + Some(CLIError::Generic(Box::new(e))), + ) + } +} From 3ccb53c2cf555e0a5fc8a134a159975e4cefebda Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 13:12:45 +0200 Subject: [PATCH 6/8] Result in proper error when parsing default configuration --- topiary-cli/src/configuration.rs | 2 +- topiary-cli/src/main.rs | 2 +- topiary-playground/build.rs | 1 + topiary-playground/src/lib.rs | 2 +- topiary/benches/benchmark.rs | 2 +- topiary/src/configuration.rs | 4 ++-- topiary/src/error.rs | 19 +++++++++---------- topiary/src/lib.rs | 4 ++-- topiary/tests/sample-tester.rs | 6 +++--- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/topiary-cli/src/configuration.rs b/topiary-cli/src/configuration.rs index 377d45a7..d6c6bde8 100644 --- a/topiary-cli/src/configuration.rs +++ b/topiary-cli/src/configuration.rs @@ -2,7 +2,7 @@ use serde_toml_merge::merge; use std::{env::current_dir, path::PathBuf}; use topiary::{default_configuration_toml, Configuration}; -use crate::error::{CLIError, CLIResult, TopiaryError}; +use crate::error::{CLIResult, TopiaryError}; pub fn parse_configuration() -> CLIResult { user_configuration_toml()? diff --git a/topiary-cli/src/main.rs b/topiary-cli/src/main.rs index bdc1873b..15abcab5 100644 --- a/topiary-cli/src/main.rs +++ b/topiary-cli/src/main.rs @@ -87,7 +87,7 @@ async fn run() -> CLIResult<()> { env_logger::init(); let args = Args::parse(); - let configuration = parse_configuration(); + let configuration = parse_configuration()?; if args.output_configuration { eprintln!("{:#?}", configuration); diff --git a/topiary-playground/build.rs b/topiary-playground/build.rs index 44227c2a..e2d46b4d 100644 --- a/topiary-playground/build.rs +++ b/topiary-playground/build.rs @@ -52,6 +52,7 @@ fn main() { let path = path.unwrap().path(); if let Some(ext) = path.extension().map(|ext| ext.to_string_lossy()) { if !Configuration::parse_default_configuration() + .unwrap() .known_extensions() .contains(&*ext) || ext == "mli" diff --git a/topiary-playground/src/lib.rs b/topiary-playground/src/lib.rs index fcf41ad5..43042715 100644 --- a/topiary-playground/src/lib.rs +++ b/topiary-playground/src/lib.rs @@ -45,7 +45,7 @@ async fn format_inner( ) -> FormatterResult { let mut output = Vec::new(); - let configuration = Configuration::parse_default_configuration(); + let configuration = Configuration::parse_default_configuration()?; let language = configuration.get_language(language_name)?; let grammars = language.grammars_wasm().await?; diff --git a/topiary/benches/benchmark.rs b/topiary/benches/benchmark.rs index 4df651b8..0eb14387 100644 --- a/topiary/benches/benchmark.rs +++ b/topiary/benches/benchmark.rs @@ -8,7 +8,7 @@ use topiary::{formatter, Operation}; async fn format() { let input = fs::read_to_string("tests/samples/input/ocaml.ml").unwrap(); let query = fs::read_to_string("../languages/ocaml.scm").unwrap(); - let configuration = Configuration::parse_default_configuration(); + let configuration = Configuration::parse_default_configuration().unwrap(); let language = configuration.get_language("ocaml_implementation").unwrap(); let grammars = language.grammars().await.unwrap(); diff --git a/topiary/src/configuration.rs b/topiary/src/configuration.rs index d11cf3c0..381a4fe4 100644 --- a/topiary/src/configuration.rs +++ b/topiary/src/configuration.rs @@ -35,10 +35,10 @@ impl Configuration { )); } - pub fn parse_default_configuration() -> Self { + pub fn parse_default_configuration() -> FormatterResult { default_configuration_toml() .try_into() - .expect("TODO: Error") + .map_err(FormatterError::from) } } diff --git a/topiary/src/error.rs b/topiary/src/error.rs index 47638e21..8e53c36f 100644 --- a/topiary/src/error.rs +++ b/topiary/src/error.rs @@ -40,9 +40,6 @@ pub enum FormatterError { /// The configuration file or command line mentions an unsupported language UnsupportedLanguage(String), - - /// Configuration file parsing error - ConfigurationParsing(PathBuf, toml::de::Error), } /// A subtype of `FormatterError::Io` @@ -113,12 +110,6 @@ impl fmt::Display for FormatterError { Self::UnsupportedLanguage(language) => { write!(f, "The following language is not supported: {language}") } - - Self::ConfigurationParsing(filepath, err) => { - let filepath = filepath.to_string_lossy(); - let message = err.message(); - write!(f, "Could not parse the configuration file at {filepath}. Parsing returned the following error: {message}") - } } } } @@ -137,7 +128,6 @@ impl Error for FormatterError { Self::Io(IoError::Filesystem(_, source)) => Some(source), Self::Io(IoError::Generic(_, Some(source))) => Some(source.as_ref()), Self::Formatting(err) => Some(err), - Self::ConfigurationParsing(_, err) => Some(err), } } } @@ -217,3 +207,12 @@ impl From for FormatterError { Self::Internal("Error while parsing".into(), Some(Box::new(e))) } } + +impl From for FormatterError { + fn from(e: toml::de::Error) -> Self { + Self::Internal( + "Error while parsing the internal configuration file".to_owned(), + Some(Box::new(e)), + ) + } +} diff --git a/topiary/src/lib.rs b/topiary/src/lib.rs index efd12d8f..b8794769 100644 --- a/topiary/src/lib.rs +++ b/topiary/src/lib.rs @@ -134,7 +134,7 @@ pub enum Operation { /// let mut query = String::new(); /// query_file.read_to_string(&mut query).expect("read query file"); /// -/// let config = Configuration::parse_default_configuration(); +/// let config = Configuration::parse_default_configuration().unwrap(); /// let language = config.get_language("json").unwrap(); /// let grammars = language /// .grammars() @@ -276,7 +276,7 @@ mod test { let mut input = "[ 1, % ]".as_bytes(); let mut output = Vec::new(); let query = "(#language! json)"; - let configuration = Configuration::parse_default_configuration(); + let configuration = Configuration::parse_default_configuration().unwrap(); let language = configuration.get_language("json").unwrap(); let grammars = language.grammars().await.unwrap(); diff --git a/topiary/tests/sample-tester.rs b/topiary/tests/sample-tester.rs index d881e0e6..16a91319 100644 --- a/topiary/tests/sample-tester.rs +++ b/topiary/tests/sample-tester.rs @@ -27,7 +27,7 @@ fn pretty_assert_eq(v1: &str, v2: &str) { async fn input_output_tester() { let input_dir = fs::read_dir("tests/samples/input").unwrap(); let expected_dir = Path::new("tests/samples/expected"); - let config = Configuration::parse_default_configuration(); + let config = Configuration::parse_default_configuration().unwrap(); let extensions = config.known_extensions(); for file in input_dir { @@ -71,7 +71,7 @@ async fn input_output_tester() { // Test that our query files are properly formatted #[test(tokio::test)] async fn formatted_query_tester() { - let config = Configuration::parse_default_configuration(); + let config = Configuration::parse_default_configuration().unwrap(); let language_dir = fs::read_dir("../languages").unwrap(); for file in language_dir { @@ -108,7 +108,7 @@ async fn formatted_query_tester() { // Test that all queries are used on sample files #[test(tokio::test)] async fn exhaustive_query_tester() { - let config = Configuration::parse_default_configuration(); + let config = Configuration::parse_default_configuration().unwrap(); let input_dir = fs::read_dir("tests/samples/input").unwrap(); for file in input_dir { From 1bbaefdfa9a84c24846546992b997c322c66c7d9 Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 13:22:34 +0200 Subject: [PATCH 7/8] Gracefully fail at merging --- topiary-cli/src/configuration.rs | 4 +--- topiary-cli/src/error.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/topiary-cli/src/configuration.rs b/topiary-cli/src/configuration.rs index d6c6bde8..bafc5a17 100644 --- a/topiary-cli/src/configuration.rs +++ b/topiary-cli/src/configuration.rs @@ -22,9 +22,7 @@ fn user_configuration_toml() -> CLIResult { }) .collect::, _>>()? .into_iter() - .fold(default_configuration_toml(), |a, b| { - merge(a, b).expect("TODO: Gracefull fail") - }); + .try_fold(default_configuration_toml(), |a, b| merge(a, b))?; Ok(config) } diff --git a/topiary-cli/src/error.rs b/topiary-cli/src/error.rs index 4d5acbf8..52922280 100644 --- a/topiary-cli/src/error.rs +++ b/topiary-cli/src/error.rs @@ -125,3 +125,12 @@ impl From for TopiaryError { ) } } + +impl From for TopiaryError { + fn from(e: serde_toml_merge::Error) -> Self { + TopiaryError::Bin( + format!("Could not merge the default configuration and user configurations. Error occured while merging: {}", e.path), + None, + ) + } +} From 1c8b4eef708aab63ab60f2cc28609406ed23673b Mon Sep 17 00:00:00 2001 From: Erin van der Veen Date: Wed, 14 Jun 2023 13:49:15 +0200 Subject: [PATCH 8/8] Read languages.toml from users configuration directory --- Cargo.lock | 28 ++++++++++ Cargo.toml | 1 + topiary-cli/Cargo.toml | 1 + topiary-cli/src/configuration.rs | 88 ++++++++++++++++++++++++++++++-- 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17576db6..79c5f763 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,6 +411,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -421,6 +430,18 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -786,6 +807,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_str_bytes" version = "6.5.1" @@ -1404,6 +1431,7 @@ version = "0.2.2" dependencies = [ "assert_cmd", "clap 4.3.3", + "directories", "env_logger", "log", "serde-toml-merge", diff --git a/Cargo.toml b/Cargo.toml index 00383623..cec1972e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ assert_cmd = "2.0" cfg-if = "1.0.0" clap = "4.3" criterion = "0.4" +directories = "5.0" env_logger = "0.10" futures = "0.3.28" itertools = "0.10" diff --git a/topiary-cli/Cargo.toml b/topiary-cli/Cargo.toml index 30c79406..f69396c7 100644 --- a/topiary-cli/Cargo.toml +++ b/topiary-cli/Cargo.toml @@ -28,6 +28,7 @@ path = "src/main.rs" # Eventually we will want to dynamically load them, like Helix does. clap = { workspace = true, features = ["derive"] } env_logger = { workspace = true } +directories = { workspace = true } log = { workspace = true } serde-toml-merge = { workspace = true } tempfile = { workspace = true } diff --git a/topiary-cli/src/configuration.rs b/topiary-cli/src/configuration.rs index bafc5a17..17d6d26d 100644 --- a/topiary-cli/src/configuration.rs +++ b/topiary-cli/src/configuration.rs @@ -1,4 +1,4 @@ -use serde_toml_merge::merge; +use directories::ProjectDirs; use std::{env::current_dir, path::PathBuf}; use topiary::{default_configuration_toml, Configuration}; @@ -12,7 +12,7 @@ pub fn parse_configuration() -> CLIResult { /// User configured languages.toml file, merged with the default config. fn user_configuration_toml() -> CLIResult { - let config = [find_workspace().join(".topiary")] + let config = [find_configuration_dir(), find_workspace().join(".topiary")] .into_iter() .map(|path| path.join("languages.toml")) .filter_map(|file| { @@ -22,11 +22,93 @@ fn user_configuration_toml() -> CLIResult { }) .collect::, _>>()? .into_iter() - .try_fold(default_configuration_toml(), |a, b| merge(a, b))?; + .fold(default_configuration_toml(), |a, b| { + merge_toml_values(a, b, 3) + }); Ok(config) } +/// Merge two TOML documents, merging values from `right` onto `left` +/// +/// When an array exists in both `left` and `right`, `right`'s array is +/// used. When a table exists in both `left` and `right`, the merged table +/// consists of all keys in `left`'s table unioned with all keys in `right` +/// with the values of `right` being merged recursively onto values of +/// `left`. +/// +/// `merge_toplevel_arrays` controls whether a top-level array in the TOML +/// document is merged instead of overridden. This is useful for TOML +/// documents that use a top-level array of values like the `languages.toml`, +/// where one usually wants to override or add to the array instead of +/// replacing it altogether. +/// +/// NOTE: This merge function is taken from Helix: +/// https://github.com/helix-editor/helix licensed under MPL-2.0. There +/// it is defined under: helix-loader/src/lib.rs. Taken from commit df09490 +pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { + use toml::Value; + + fn get_name(v: &Value) -> Option<&str> { + v.get("name").and_then(Value::as_str) + } + + match (left, right) { + (Value::Array(mut left_items), Value::Array(right_items)) => { + // The top-level arrays should be merged but nested arrays should + // act as overrides. For the `languages.toml` config, this means + // that you can specify a sub-set of languages in an overriding + // `languages.toml` but that nested arrays like file extensions + // arguments are replaced instead of merged. + if merge_depth > 0 { + left_items.reserve(right_items.len()); + for rvalue in right_items { + let lvalue = get_name(&rvalue) + .and_then(|rname| { + left_items.iter().position(|v| get_name(v) == Some(rname)) + }) + .map(|lpos| left_items.remove(lpos)); + let mvalue = match lvalue { + Some(lvalue) => merge_toml_values(lvalue, rvalue, merge_depth - 1), + None => rvalue, + }; + left_items.push(mvalue); + } + Value::Array(left_items) + } else { + Value::Array(right_items) + } + } + (Value::Table(mut left_map), Value::Table(right_map)) => { + if merge_depth > 0 { + for (rname, rvalue) in right_map { + match left_map.remove(&rname) { + Some(lvalue) => { + let merged_value = merge_toml_values(lvalue, rvalue, merge_depth - 1); + left_map.insert(rname, merged_value); + } + None => { + left_map.insert(rname, rvalue); + } + } + } + Value::Table(left_map) + } else { + Value::Table(right_map) + } + } + // Catch everything else we didn't handle, and use the right value + (_, value) => value, + } +} + +fn find_configuration_dir() -> PathBuf { + ProjectDirs::from("", "", "topiary") + .expect("Could not access the OS's Home directory") + .config_dir() + .to_owned() +} + pub fn find_workspace() -> PathBuf { let current_dir = current_dir().expect("Could not get current working directory"); for ancestor in current_dir.ancestors() {