Skip to content

Add djls-conf crate and add initial settings #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/

## [Unreleased]

### Added

- Added support for loading server settings from user files (`~/.config/djls/djls.toml`) and project files (`djls.toml`, `.djls.toml`, and `pyproject.toml` via `[tool.djls]` table).
- Implemented dynamic settings reloading via `workspace/didChangeConfiguration`.

### Changed

- **Internal**: Moved task queueing functionality to `djls-server` crate, renamed from `Worker` to `Queue`, and simplified API.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ resolver = "2"

[workspace.dependencies]
djls = { path = "crates/djls" }
djls-conf = { path = "crates/djls-conf" }
djls-project = { path = "crates/djls-project" }
djls-server = { path = "crates/djls-server" }
djls-templates = { path = "crates/djls-templates" }
Expand Down
16 changes: 16 additions & 0 deletions crates/djls-conf/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "djls-conf"
version = "0.0.0"
edition = "2021"

[dependencies]
anyhow = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

config = { version ="0.15", features = ["toml"] }
directories = "6.0"
toml = "0.8"

[dev-dependencies]
tempfile = "3.19"
245 changes: 245 additions & 0 deletions crates/djls-conf/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
use config::{Config, ConfigError as ExternalConfigError, File, FileFormat};
use directories::ProjectDirs;
use serde::Deserialize;
use std::{fs, path::Path};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Configuration build/deserialize error")]
Config(#[from] ExternalConfigError),
#[error("Failed to read pyproject.toml")]
PyprojectIo(#[from] std::io::Error),
#[error("Failed to parse pyproject.toml TOML")]
PyprojectParse(#[from] toml::de::Error),
#[error("Failed to serialize extracted pyproject.toml data")]
PyprojectSerialize(#[from] toml::ser::Error),
}

#[derive(Debug, Deserialize, Default, PartialEq)]
#[serde(default)]
pub struct Settings {
debug: bool,
}

impl Settings {
pub fn new(project_root: &Path) -> Result<Self, ConfigError> {
let user_config_file = ProjectDirs::from("com.github", "joshuadavidthomas", "djls")
.map(|proj_dirs| proj_dirs.config_dir().join("djls.toml"));

Self::load_from_paths(project_root, user_config_file.as_deref())
}

fn load_from_paths(
project_root: &Path,
user_config_path: Option<&Path>,
) -> Result<Self, ConfigError> {
let mut builder = Config::builder();

if let Some(path) = user_config_path {
builder = builder.add_source(File::from(path).format(FileFormat::Toml).required(false));
}

let pyproject_path = project_root.join("pyproject.toml");
if pyproject_path.exists() {
let content = fs::read_to_string(&pyproject_path)?;
let toml_str: toml::Value = toml::from_str(&content)?;
let tool_djls_value: Option<&toml::Value> =
["tool", "djls"].iter().try_fold(&toml_str, |val, &key| {
// Attempt to get the next key. If it exists, return Some(value) to continue.
// If get returns None, try_fold automatically stops and returns None overall.
val.get(key)
});
if let Some(tool_djls_table) = tool_djls_value.and_then(|v| v.as_table()) {
let tool_djls_string = toml::to_string(tool_djls_table)?;
builder = builder.add_source(File::from_str(&tool_djls_string, FileFormat::Toml));
}
}

builder = builder.add_source(
File::from(project_root.join(".djls.toml"))
.format(FileFormat::Toml)
.required(false),
);

builder = builder.add_source(
File::from(project_root.join("djls.toml"))
.format(FileFormat::Toml)
.required(false),
);

let config = builder.build()?;
let settings = config.try_deserialize()?;
Ok(settings)
}

pub fn debug(&self) -> bool {
self.debug
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;

mod defaults {
use super::*;

#[test]
fn test_load_no_files() {
let dir = tempdir().unwrap();
let settings = Settings::new(dir.path()).unwrap();
// Should load defaults
assert_eq!(settings, Settings { debug: false });
// Add assertions for future default fields here
}
}

mod project_files {
use super::*;

#[test]
fn test_load_djls_toml_only() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("djls.toml"), "debug = true").unwrap();
let settings = Settings::new(dir.path()).unwrap();
assert_eq!(settings, Settings { debug: true });
}

#[test]
fn test_load_dot_djls_toml_only() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".djls.toml"), "debug = true").unwrap();
let settings = Settings::new(dir.path()).unwrap();
assert_eq!(settings, Settings { debug: true });
}

#[test]
fn test_load_pyproject_toml_only() {
let dir = tempdir().unwrap();
// Write the setting under [tool.djls]
let content = "[tool.djls]\ndebug = true\n";
fs::write(dir.path().join("pyproject.toml"), content).unwrap();
let settings = Settings::new(dir.path()).unwrap();
assert_eq!(settings, Settings { debug: true });
}
}

mod priority {
use super::*;

#[test]
fn test_project_priority_djls_overrides_dot_djls() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".djls.toml"), "debug = false").unwrap();
fs::write(dir.path().join("djls.toml"), "debug = true").unwrap();
let settings = Settings::new(dir.path()).unwrap();
assert_eq!(settings, Settings { debug: true }); // djls.toml wins
}

#[test]
fn test_project_priority_dot_djls_overrides_pyproject() {
let dir = tempdir().unwrap();
let pyproject_content = "[tool.djls]\ndebug = false\n";
fs::write(dir.path().join("pyproject.toml"), pyproject_content).unwrap();
fs::write(dir.path().join(".djls.toml"), "debug = true").unwrap();
let settings = Settings::new(dir.path()).unwrap();
assert_eq!(settings, Settings { debug: true }); // .djls.toml wins
}

#[test]
fn test_project_priority_all_files_djls_wins() {
let dir = tempdir().unwrap();
let pyproject_content = "[tool.djls]\ndebug = false\n";
fs::write(dir.path().join("pyproject.toml"), pyproject_content).unwrap();
fs::write(dir.path().join(".djls.toml"), "debug = false").unwrap();
fs::write(dir.path().join("djls.toml"), "debug = true").unwrap();
let settings = Settings::new(dir.path()).unwrap();
assert_eq!(settings, Settings { debug: true }); // djls.toml wins
}

#[test]
fn test_user_priority_project_overrides_user() {
let user_dir = tempdir().unwrap();
let project_dir = tempdir().unwrap();
let user_conf_path = user_dir.path().join("config.toml");
fs::write(&user_conf_path, "debug = true").unwrap(); // User: true
let pyproject_content = "[tool.djls]\ndebug = false\n"; // Project: false
fs::write(project_dir.path().join("pyproject.toml"), pyproject_content).unwrap();

let settings =
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
assert_eq!(settings, Settings { debug: false }); // pyproject.toml overrides user
}

#[test]
fn test_user_priority_djls_overrides_user() {
let user_dir = tempdir().unwrap();
let project_dir = tempdir().unwrap();
let user_conf_path = user_dir.path().join("config.toml");
fs::write(&user_conf_path, "debug = true").unwrap(); // User: true
fs::write(project_dir.path().join("djls.toml"), "debug = false").unwrap(); // Project: false

let settings =
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
assert_eq!(settings, Settings { debug: false }); // djls.toml overrides user
}
}

mod user_config {
use super::*;

#[test]
fn test_load_user_config_only() {
let user_dir = tempdir().unwrap();
let project_dir = tempdir().unwrap(); // Empty project dir
let user_conf_path = user_dir.path().join("config.toml");
fs::write(&user_conf_path, "debug = true").unwrap();

let settings =
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
assert_eq!(settings, Settings { debug: true });
}

#[test]
fn test_no_user_config_file_present() {
let user_dir = tempdir().unwrap(); // Exists, but no config.toml inside
let project_dir = tempdir().unwrap();
let user_conf_path = user_dir.path().join("config.toml"); // Path exists, file doesn't
let pyproject_content = "[tool.djls]\ndebug = true\n";
fs::write(project_dir.path().join("pyproject.toml"), pyproject_content).unwrap();

// Should load project settings fine, ignoring non-existent user config
let settings =
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
assert_eq!(settings, Settings { debug: true });
}

#[test]
fn test_user_config_path_not_provided() {
// Simulates ProjectDirs::from returning None
let project_dir = tempdir().unwrap();
fs::write(project_dir.path().join("djls.toml"), "debug = true").unwrap();

// Call helper with None for user path
let settings = Settings::load_from_paths(project_dir.path(), None).unwrap();
assert_eq!(settings, Settings { debug: true });
}
}

mod errors {
use super::*;

#[test]
fn test_invalid_toml_content() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("djls.toml"), "debug = not_a_boolean").unwrap();
// Need to call Settings::new here as load_from_paths doesn't involve ProjectDirs
let result = Settings::new(dir.path());
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ConfigError::Config(_)));
}
}
}
1 change: 1 addition & 0 deletions crates/djls-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
djls-conf = { workspace = true }
djls-project = { workspace = true }
djls-templates = { workspace = true }

Expand Down
Loading
Loading