Skip to content

Commit 3008389

Browse files
Add djls-conf crate and add initial settings (#113)
1 parent b83ed62 commit 3008389

File tree

6 files changed

+339
-1
lines changed

6 files changed

+339
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- 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).
24+
- Implemented dynamic settings reloading via `workspace/didChangeConfiguration`.
25+
2126
### Changed
2227

2328
- **Internal**: Moved task queueing functionality to `djls-server` crate, renamed from `Worker` to `Queue`, and simplified API.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ resolver = "2"
44

55
[workspace.dependencies]
66
djls = { path = "crates/djls" }
7+
djls-conf = { path = "crates/djls-conf" }
78
djls-project = { path = "crates/djls-project" }
89
djls-server = { path = "crates/djls-server" }
910
djls-templates = { path = "crates/djls-templates" }

crates/djls-conf/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "djls-conf"
3+
version = "0.0.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = { workspace = true }
8+
serde = { workspace = true }
9+
thiserror = { workspace = true }
10+
11+
config = { version ="0.15", features = ["toml"] }
12+
directories = "6.0"
13+
toml = "0.8"
14+
15+
[dev-dependencies]
16+
tempfile = "3.19"

crates/djls-conf/src/lib.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
use config::{Config, ConfigError as ExternalConfigError, File, FileFormat};
2+
use directories::ProjectDirs;
3+
use serde::Deserialize;
4+
use std::{fs, path::Path};
5+
use thiserror::Error;
6+
7+
#[derive(Error, Debug)]
8+
pub enum ConfigError {
9+
#[error("Configuration build/deserialize error")]
10+
Config(#[from] ExternalConfigError),
11+
#[error("Failed to read pyproject.toml")]
12+
PyprojectIo(#[from] std::io::Error),
13+
#[error("Failed to parse pyproject.toml TOML")]
14+
PyprojectParse(#[from] toml::de::Error),
15+
#[error("Failed to serialize extracted pyproject.toml data")]
16+
PyprojectSerialize(#[from] toml::ser::Error),
17+
}
18+
19+
#[derive(Debug, Deserialize, Default, PartialEq)]
20+
#[serde(default)]
21+
pub struct Settings {
22+
debug: bool,
23+
}
24+
25+
impl Settings {
26+
pub fn new(project_root: &Path) -> Result<Self, ConfigError> {
27+
let user_config_file = ProjectDirs::from("com.github", "joshuadavidthomas", "djls")
28+
.map(|proj_dirs| proj_dirs.config_dir().join("djls.toml"));
29+
30+
Self::load_from_paths(project_root, user_config_file.as_deref())
31+
}
32+
33+
fn load_from_paths(
34+
project_root: &Path,
35+
user_config_path: Option<&Path>,
36+
) -> Result<Self, ConfigError> {
37+
let mut builder = Config::builder();
38+
39+
if let Some(path) = user_config_path {
40+
builder = builder.add_source(File::from(path).format(FileFormat::Toml).required(false));
41+
}
42+
43+
let pyproject_path = project_root.join("pyproject.toml");
44+
if pyproject_path.exists() {
45+
let content = fs::read_to_string(&pyproject_path)?;
46+
let toml_str: toml::Value = toml::from_str(&content)?;
47+
let tool_djls_value: Option<&toml::Value> =
48+
["tool", "djls"].iter().try_fold(&toml_str, |val, &key| {
49+
// Attempt to get the next key. If it exists, return Some(value) to continue.
50+
// If get returns None, try_fold automatically stops and returns None overall.
51+
val.get(key)
52+
});
53+
if let Some(tool_djls_table) = tool_djls_value.and_then(|v| v.as_table()) {
54+
let tool_djls_string = toml::to_string(tool_djls_table)?;
55+
builder = builder.add_source(File::from_str(&tool_djls_string, FileFormat::Toml));
56+
}
57+
}
58+
59+
builder = builder.add_source(
60+
File::from(project_root.join(".djls.toml"))
61+
.format(FileFormat::Toml)
62+
.required(false),
63+
);
64+
65+
builder = builder.add_source(
66+
File::from(project_root.join("djls.toml"))
67+
.format(FileFormat::Toml)
68+
.required(false),
69+
);
70+
71+
let config = builder.build()?;
72+
let settings = config.try_deserialize()?;
73+
Ok(settings)
74+
}
75+
76+
pub fn debug(&self) -> bool {
77+
self.debug
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
use std::fs;
85+
use tempfile::tempdir;
86+
87+
mod defaults {
88+
use super::*;
89+
90+
#[test]
91+
fn test_load_no_files() {
92+
let dir = tempdir().unwrap();
93+
let settings = Settings::new(dir.path()).unwrap();
94+
// Should load defaults
95+
assert_eq!(settings, Settings { debug: false });
96+
// Add assertions for future default fields here
97+
}
98+
}
99+
100+
mod project_files {
101+
use super::*;
102+
103+
#[test]
104+
fn test_load_djls_toml_only() {
105+
let dir = tempdir().unwrap();
106+
fs::write(dir.path().join("djls.toml"), "debug = true").unwrap();
107+
let settings = Settings::new(dir.path()).unwrap();
108+
assert_eq!(settings, Settings { debug: true });
109+
}
110+
111+
#[test]
112+
fn test_load_dot_djls_toml_only() {
113+
let dir = tempdir().unwrap();
114+
fs::write(dir.path().join(".djls.toml"), "debug = true").unwrap();
115+
let settings = Settings::new(dir.path()).unwrap();
116+
assert_eq!(settings, Settings { debug: true });
117+
}
118+
119+
#[test]
120+
fn test_load_pyproject_toml_only() {
121+
let dir = tempdir().unwrap();
122+
// Write the setting under [tool.djls]
123+
let content = "[tool.djls]\ndebug = true\n";
124+
fs::write(dir.path().join("pyproject.toml"), content).unwrap();
125+
let settings = Settings::new(dir.path()).unwrap();
126+
assert_eq!(settings, Settings { debug: true });
127+
}
128+
}
129+
130+
mod priority {
131+
use super::*;
132+
133+
#[test]
134+
fn test_project_priority_djls_overrides_dot_djls() {
135+
let dir = tempdir().unwrap();
136+
fs::write(dir.path().join(".djls.toml"), "debug = false").unwrap();
137+
fs::write(dir.path().join("djls.toml"), "debug = true").unwrap();
138+
let settings = Settings::new(dir.path()).unwrap();
139+
assert_eq!(settings, Settings { debug: true }); // djls.toml wins
140+
}
141+
142+
#[test]
143+
fn test_project_priority_dot_djls_overrides_pyproject() {
144+
let dir = tempdir().unwrap();
145+
let pyproject_content = "[tool.djls]\ndebug = false\n";
146+
fs::write(dir.path().join("pyproject.toml"), pyproject_content).unwrap();
147+
fs::write(dir.path().join(".djls.toml"), "debug = true").unwrap();
148+
let settings = Settings::new(dir.path()).unwrap();
149+
assert_eq!(settings, Settings { debug: true }); // .djls.toml wins
150+
}
151+
152+
#[test]
153+
fn test_project_priority_all_files_djls_wins() {
154+
let dir = tempdir().unwrap();
155+
let pyproject_content = "[tool.djls]\ndebug = false\n";
156+
fs::write(dir.path().join("pyproject.toml"), pyproject_content).unwrap();
157+
fs::write(dir.path().join(".djls.toml"), "debug = false").unwrap();
158+
fs::write(dir.path().join("djls.toml"), "debug = true").unwrap();
159+
let settings = Settings::new(dir.path()).unwrap();
160+
assert_eq!(settings, Settings { debug: true }); // djls.toml wins
161+
}
162+
163+
#[test]
164+
fn test_user_priority_project_overrides_user() {
165+
let user_dir = tempdir().unwrap();
166+
let project_dir = tempdir().unwrap();
167+
let user_conf_path = user_dir.path().join("config.toml");
168+
fs::write(&user_conf_path, "debug = true").unwrap(); // User: true
169+
let pyproject_content = "[tool.djls]\ndebug = false\n"; // Project: false
170+
fs::write(project_dir.path().join("pyproject.toml"), pyproject_content).unwrap();
171+
172+
let settings =
173+
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
174+
assert_eq!(settings, Settings { debug: false }); // pyproject.toml overrides user
175+
}
176+
177+
#[test]
178+
fn test_user_priority_djls_overrides_user() {
179+
let user_dir = tempdir().unwrap();
180+
let project_dir = tempdir().unwrap();
181+
let user_conf_path = user_dir.path().join("config.toml");
182+
fs::write(&user_conf_path, "debug = true").unwrap(); // User: true
183+
fs::write(project_dir.path().join("djls.toml"), "debug = false").unwrap(); // Project: false
184+
185+
let settings =
186+
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
187+
assert_eq!(settings, Settings { debug: false }); // djls.toml overrides user
188+
}
189+
}
190+
191+
mod user_config {
192+
use super::*;
193+
194+
#[test]
195+
fn test_load_user_config_only() {
196+
let user_dir = tempdir().unwrap();
197+
let project_dir = tempdir().unwrap(); // Empty project dir
198+
let user_conf_path = user_dir.path().join("config.toml");
199+
fs::write(&user_conf_path, "debug = true").unwrap();
200+
201+
let settings =
202+
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
203+
assert_eq!(settings, Settings { debug: true });
204+
}
205+
206+
#[test]
207+
fn test_no_user_config_file_present() {
208+
let user_dir = tempdir().unwrap(); // Exists, but no config.toml inside
209+
let project_dir = tempdir().unwrap();
210+
let user_conf_path = user_dir.path().join("config.toml"); // Path exists, file doesn't
211+
let pyproject_content = "[tool.djls]\ndebug = true\n";
212+
fs::write(project_dir.path().join("pyproject.toml"), pyproject_content).unwrap();
213+
214+
// Should load project settings fine, ignoring non-existent user config
215+
let settings =
216+
Settings::load_from_paths(project_dir.path(), Some(&user_conf_path)).unwrap();
217+
assert_eq!(settings, Settings { debug: true });
218+
}
219+
220+
#[test]
221+
fn test_user_config_path_not_provided() {
222+
// Simulates ProjectDirs::from returning None
223+
let project_dir = tempdir().unwrap();
224+
fs::write(project_dir.path().join("djls.toml"), "debug = true").unwrap();
225+
226+
// Call helper with None for user path
227+
let settings = Settings::load_from_paths(project_dir.path(), None).unwrap();
228+
assert_eq!(settings, Settings { debug: true });
229+
}
230+
}
231+
232+
mod errors {
233+
use super::*;
234+
235+
#[test]
236+
fn test_invalid_toml_content() {
237+
let dir = tempdir().unwrap();
238+
fs::write(dir.path().join("djls.toml"), "debug = not_a_boolean").unwrap();
239+
// Need to call Settings::new here as load_from_paths doesn't involve ProjectDirs
240+
let result = Settings::new(dir.path());
241+
assert!(result.is_err());
242+
assert!(matches!(result.unwrap_err(), ConfigError::Config(_)));
243+
}
244+
}
245+
}

crates/djls-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
djls-conf = { workspace = true }
78
djls-project = { workspace = true }
89
djls-templates = { workspace = true }
910

0 commit comments

Comments
 (0)