use std::path::{Path, PathBuf};
use std::str::FromStr;
use configparser::ini::Ini;
use hashbrown::HashMap;
use itertools::Itertools;
use sqruff_lib_core::dialects::Dialect;
use sqruff_lib_core::dialects::init::{DialectKind, dialect_readout};
use sqruff_lib_core::errors::SQLFluffUserError;
use sqruff_lib_core::parser::{IndentationConfig, Parser};
pub use sqruff_lib_core::value::Value;
use sqruff_lib_dialects::kind_to_dialect;
use crate::templaters::TemplaterKind;
use crate::utils::reflow::config::ReflowConfig;
pub fn split_comma_separated_string(raw_str: &str) -> Value {
let values = raw_str
.split(',')
.filter_map(|x| {
let trimmed = x.trim();
(!trimmed.is_empty()).then(|| Value::String(trimmed.into()))
})
.collect();
Value::Array(values)
}
#[derive(Debug, PartialEq, Clone)]
pub struct FluffConfig {
pub(crate) indentation: FluffConfigIndentation,
pub raw: HashMap<String, Value>,
extra_config_path: Option<String>,
_configs: HashMap<String, HashMap<String, String>>,
pub(crate) dialect: Dialect,
sql_file_exts: Vec<String>,
reflow: ReflowConfig,
}
impl Default for FluffConfig {
fn default() -> Self {
Self::new(<_>::default(), None, None)
}
}
impl FluffConfig {
fn configured_dialect_kind_from_raw(configs: &HashMap<String, Value>) -> DialectKind {
match configs
.get("core")
.and_then(|map| map.as_map().unwrap().get("dialect"))
{
None => DialectKind::default(),
Some(Value::String(std)) => DialectKind::from_str(std).unwrap(),
_value => DialectKind::default(),
}
}
fn dialect_section_from_raw(
configs: &HashMap<String, Value>,
dialect_kind: DialectKind,
) -> Option<&Value> {
configs
.get("dialect")
.and_then(|v| v.as_map())
.and_then(|m| m.get(dialect_kind.as_ref()))
}
pub fn override_dialect(&mut self, dialect: DialectKind) -> Result<(), String> {
self.dialect = kind_to_dialect(&dialect, None)
.ok_or(format!("Invalid dialect: {}", dialect.as_ref()))?;
Ok(())
}
pub fn get(&self, key: &str, section: &str) -> &Value {
&self.raw[section][key]
}
pub fn reflow(&self) -> &ReflowConfig {
&self.reflow
}
fn templater_root_section(&self) -> Option<&HashMap<String, Value>> {
self.raw.get("templater").and_then(Value::as_map)
}
pub fn templater_root_value(&self, key: &str) -> Option<&Value> {
self.templater_root_section()?.get(key)
}
pub fn templater_section(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
self.templater_root_section()?
.get(templater.as_str())
.and_then(Value::as_map)
}
pub fn templater_value(&self, templater: TemplaterKind, key: &str) -> Option<&Value> {
self.templater_section(templater)?.get(key)
}
pub fn templater_context(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
self.templater_value(templater, "context")
.and_then(Value::as_map)
}
pub fn reload_reflow(&mut self) {
self.reflow = ReflowConfig::from_fluff_config(self);
}
pub fn from_file(path: &Path) -> FluffConfig {
let mut configs = HashMap::new();
ConfigLoader::load_config_file(path, &mut configs);
FluffConfig::new(configs, None, None)
}
pub fn from_source(source: &str, optional_path_specification: Option<&Path>) -> FluffConfig {
let configs = ConfigLoader::from_source(source, optional_path_specification);
FluffConfig::new(configs, None, None)
}
pub fn get_section(&self, section: &str) -> &HashMap<String, Value> {
self.raw[section].as_map().unwrap()
}
pub fn dialect_kind(&self) -> DialectKind {
self.dialect.name()
}
pub fn templater_kind(&self) -> Result<TemplaterKind, String> {
self.get("templater", "core")
.as_string()
.map(TemplaterKind::from_name)
.transpose()
.map(|templater| templater.unwrap_or(TemplaterKind::Raw))
}
pub fn dialect_section(&self, dialect_kind: DialectKind) -> Option<&Value> {
Self::dialect_section_from_raw(&self.raw, dialect_kind)
}
pub fn new(
configs: HashMap<String, Value>,
extra_config_path: Option<String>,
indentation: Option<FluffConfigIndentation>,
) -> Self {
fn nested_combine(
mut a: HashMap<String, Value>,
b: HashMap<String, Value>,
) -> HashMap<String, Value> {
for (key, value_b) in b {
match (a.get(&key), value_b) {
(Some(Value::Map(map_a)), Value::Map(map_b)) => {
let combined = nested_combine(map_a.clone(), map_b);
a.insert(key, Value::Map(combined));
}
(_, value) => {
a.insert(key, value);
}
}
}
a
}
let values = ConfigLoader::get_config_elems_from_file(
None,
include_str!("./default_config.cfg").into(),
);
let mut defaults = HashMap::new();
ConfigLoader::incorporate_vals(&mut defaults, values);
let mut configs = nested_combine(defaults, configs);
let dialect_kind = Self::configured_dialect_kind_from_raw(&configs);
let dialect_config = Self::dialect_section_from_raw(&configs, dialect_kind);
let dialect = kind_to_dialect(&dialect_kind, dialect_config);
for (in_key, out_key) in [
("ignore", "ignore"),
("warnings", "warnings"),
("rules", "rule_allowlist"),
("exclude_rules", "rule_denylist"),
] {
match configs["core"].as_map().unwrap().get(in_key) {
Some(value) if !value.is_none() => {
let string = value.as_string().unwrap();
let values = split_comma_separated_string(string);
configs
.get_mut("core")
.unwrap()
.as_map_mut()
.unwrap()
.insert(out_key.into(), values);
}
_ => {}
}
}
let sql_file_exts = configs["core"]["sql_file_exts"]
.as_array()
.unwrap()
.iter()
.map(|it| it.as_string().unwrap().to_owned())
.collect();
let mut this = Self {
raw: configs,
dialect: dialect
.expect("Dialect is disabled. Please enable the corresponding feature."),
extra_config_path,
_configs: HashMap::new(),
indentation: indentation.unwrap_or_default(),
sql_file_exts,
reflow: ReflowConfig::default(),
};
this.reflow = ReflowConfig::from_fluff_config(&this);
this
}
pub fn with_sql_file_exts(mut self, exts: Vec<String>) -> Self {
self.sql_file_exts = exts;
self
}
pub fn from_root(
extra_config_path: Option<String>,
ignore_local_config: bool,
overrides: Option<HashMap<String, String>>,
) -> Result<FluffConfig, SQLFluffUserError> {
let loader = ConfigLoader {};
let mut config =
loader.load_config_up_to_path(".", extra_config_path.clone(), ignore_local_config);
if let Some(overrides) = overrides
&& let Some(dialect) = overrides.get("dialect")
{
let core = config
.entry("core".into())
.or_insert_with(|| Value::Map(HashMap::new()));
core.as_map_mut()
.unwrap()
.insert("dialect".into(), Value::String(dialect.clone().into()));
}
Ok(FluffConfig::new(config, extra_config_path, None))
}
pub fn from_kwargs(
config: Option<FluffConfig>,
dialect: Option<Dialect>,
rules: Option<Vec<String>>,
) -> Self {
if (dialect.is_some() || rules.is_some()) && config.is_some() {
panic!(
"Cannot specify `config` with `dialect` or `rules`. Any config object specifies \
its own dialect and rules."
)
} else {
config.unwrap()
}
}
pub fn process_raw_file_for_config(&self, raw_str: &str) {
for raw_line in raw_str.lines() {
if raw_line.to_string().starts_with("-- sqlfluff") {
self.process_inline_config(raw_line)
}
}
}
pub fn process_inline_config(&self, _config_line: &str) {
panic!("Not implemented")
}
pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
if self._configs.get("core")?.get("dialect").is_some() {
return None;
}
Some(SQLFluffUserError::new(format!(
"No dialect was specified. You must configure a dialect or
specify one on the command line using --dialect after the
command. Available dialects: {}",
dialect_readout().join(", ").as_str()
)))
}
pub fn get_dialect(&self) -> &Dialect {
&self.dialect
}
pub fn sql_file_exts(&self) -> &[String] {
self.sql_file_exts.as_ref()
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FluffConfigIndentation {
pub template_blocks_indent: bool,
}
impl Default for FluffConfigIndentation {
fn default() -> Self {
Self {
template_blocks_indent: true,
}
}
}
pub struct ConfigLoader;
impl ConfigLoader {
#[allow(unused_variables)]
fn iter_config_locations_up_to_path(
path: &Path,
working_path: Option<&Path>,
ignore_local_config: bool,
) -> impl Iterator<Item = PathBuf> {
let mut given_path = std::path::absolute(path).unwrap();
let working_path = std::env::current_dir().unwrap();
if !given_path.is_dir() {
given_path = given_path.parent().unwrap().into();
}
let common_path = common_path::common_path(&given_path, working_path).unwrap();
let mut path_to_visit = common_path;
let head = Some(given_path.canonicalize().unwrap()).into_iter();
let tail = std::iter::from_fn(move || {
if path_to_visit != given_path {
let path = path_to_visit.canonicalize().unwrap();
let next_path_to_visit = {
let path_to_visit_as_path = path_to_visit.as_path();
let given_path_as_path = given_path.as_path();
match given_path_as_path.strip_prefix(path_to_visit_as_path) {
Ok(relative_path) => {
if let Some(first_part) = relative_path.components().next() {
path_to_visit.join(first_part.as_os_str())
} else {
path_to_visit.clone()
}
}
Err(_) => {
path_to_visit.clone()
}
}
};
if next_path_to_visit == path_to_visit {
return None;
}
path_to_visit = next_path_to_visit;
Some(path)
} else {
None
}
});
head.chain(tail)
}
pub fn load_config_up_to_path(
&self,
path: impl AsRef<Path>,
extra_config_path: Option<String>,
ignore_local_config: bool,
) -> HashMap<String, Value> {
let path = path.as_ref();
let config_stack = if ignore_local_config {
extra_config_path
.map(|path| vec![self.load_config_at_path(path)])
.unwrap_or_default()
} else {
let configs = Self::iter_config_locations_up_to_path(path, None, ignore_local_config);
configs
.map(|path| self.load_config_at_path(path))
.collect_vec()
};
nested_combine(config_stack)
}
pub fn load_config_at_path(&self, path: impl AsRef<Path>) -> HashMap<String, Value> {
let path = path.as_ref();
let filename_options = [
".sqlfluff",
".sqruff",
];
let mut configs = HashMap::new();
if path.is_dir() {
for fname in filename_options {
let path = path.join(fname);
if path.exists() {
ConfigLoader::load_config_file(path, &mut configs);
}
}
} else if path.is_file() {
ConfigLoader::load_config_file(path, &mut configs);
};
configs
}
pub fn from_source(source: &str, path: Option<&Path>) -> HashMap<String, Value> {
let mut configs = HashMap::new();
let elems = ConfigLoader::get_config_elems_from_file(path, Some(source));
ConfigLoader::incorporate_vals(&mut configs, elems);
configs
}
pub fn load_config_file(path: impl AsRef<Path>, configs: &mut HashMap<String, Value>) {
let elems = ConfigLoader::get_config_elems_from_file(path.as_ref().into(), None);
ConfigLoader::incorporate_vals(configs, elems);
}
fn get_config_elems_from_file(
config_path: Option<&Path>,
config_string: Option<&str>,
) -> Vec<(Vec<String>, Value)> {
let mut buff = Vec::new();
let mut config = Ini::new();
let content = match (config_path, config_string) {
(None, None) | (Some(_), Some(_)) => {
unimplemented!("One of fpath or config_string is required.")
}
(None, Some(text)) => text.to_owned(),
(Some(path), None) => std::fs::read_to_string(path).unwrap(),
};
config.read(content).unwrap();
for section in config.sections() {
let key = if section == "sqlfluff" || section == "sqruff" {
vec!["core".to_owned()]
} else if let Some(key) = section
.strip_prefix("sqlfluff:")
.or_else(|| section.strip_prefix("sqruff:"))
{
key.split(':').map(ToOwned::to_owned).collect()
} else {
continue;
};
let config_map = config.get_map_ref();
if let Some(section) = config_map.get(§ion) {
for (name, value) in section {
let mut value: Value = value.as_deref().unwrap_or_default().parse().unwrap();
let name_lowercase = name.to_lowercase();
if name_lowercase == "load_macros_from_path" {
unimplemented!()
} else if name_lowercase.ends_with("_path") || name_lowercase.ends_with("_dir")
{
let path = PathBuf::from(value.as_string().unwrap());
if !path.is_absolute() {
let config_path = config_path.unwrap().parent().unwrap();
let current_dir = std::env::current_dir().unwrap();
let config_path = current_dir.join(config_path);
let config_path = std::path::absolute(config_path).unwrap();
let path = config_path.join(path);
let path: String = path.to_string_lossy().into();
value = Value::String(path.into());
}
}
let mut key = key.clone();
key.push(name.clone());
buff.push((key, value));
}
}
}
buff
}
fn incorporate_vals(ctx: &mut HashMap<String, Value>, values: Vec<(Vec<String>, Value)>) {
for (path, value) in values {
let mut current_map = &mut *ctx;
for key in path.iter().take(path.len() - 1) {
match current_map
.entry(key.to_string())
.or_insert_with(|| Value::Map(HashMap::new()))
.as_map_mut()
{
Some(slot) => current_map = slot,
None => panic!("Overriding config value with section! [{path:?}]"),
}
}
let last_key = path.last().expect("Expected at least one element in path");
current_map.insert(last_key.to_string(), value);
}
}
}
fn nested_combine(config_stack: Vec<HashMap<String, Value>>) -> HashMap<String, Value> {
let capacity = config_stack.len();
let mut result = HashMap::with_capacity(capacity);
for dict in config_stack {
for (key, value) in dict {
result.insert(key, value);
}
}
result
}
impl<'a> From<&'a FluffConfig> for Parser<'a> {
fn from(config: &'a FluffConfig) -> Self {
let dialect = config.get_dialect();
let indentation_section = &config.raw["indentation"];
let indentation_config =
IndentationConfig::from_bool_lookup(|key| indentation_section[key].to_bool());
Self::new(dialect, indentation_config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sqruff_lib_core::dialects::init::DialectKind;
#[test]
fn test_dialect_config_section_parsing() {
let config = FluffConfig::from_source(
r#"
[sqruff]
dialect = snowflake
[sqruff:dialect:snowflake]
some_option = value
"#,
None,
);
let dialect_section = config.raw.get("dialect");
assert!(dialect_section.is_some());
let snowflake_config = dialect_section.unwrap().as_map().unwrap().get("snowflake");
assert!(snowflake_config.is_some());
let snowflake_map = snowflake_config.unwrap().as_map().unwrap();
assert_eq!(
snowflake_map.get("some_option").unwrap().as_string(),
Some("value")
);
}
#[test]
fn test_dialect_config_empty_section() {
let config = FluffConfig::from_source(
r#"
[sqruff]
dialect = bigquery
[sqruff:dialect:bigquery]
"#,
None,
);
assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
}
#[test]
fn test_dialect_without_config_section() {
let config = FluffConfig::from_source(
r#"
[sqruff]
dialect = postgres
"#,
None,
);
assert_eq!(config.get_dialect().name, DialectKind::Postgres);
}
#[test]
fn test_templater_kind_defaults_to_raw() {
let config = FluffConfig::from_source("", None);
assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Raw);
}
#[test]
fn test_templater_kind_parses_placeholder() {
let config = FluffConfig::from_source(
r#"
[sqruff]
templater = placeholder
"#,
None,
);
assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
}
#[test]
fn test_templater_section_uses_typed_kind() {
let config = FluffConfig::from_source(
r#"
[sqruff]
templater = placeholder
[sqruff:templater:placeholder]
param_style = colon
"#,
None,
);
let section = config
.templater_section(TemplaterKind::Placeholder)
.unwrap();
assert_eq!(
section.get("param_style").unwrap().as_string(),
Some("colon")
);
}
#[cfg(feature = "python")]
#[test]
fn test_templater_context_uses_typed_kind() {
let config = FluffConfig::from_source(
r#"
[sqruff]
templater = python
[sqruff:templater:python:context]
blah = foo
"#,
None,
);
let context = config.templater_context(TemplaterKind::Python).unwrap();
assert_eq!(context.get("blah").unwrap().as_string(), Some("foo"));
}
}