Skip to content

Commit bf39fd6

Browse files
updates
1 parent b04a589 commit bf39fd6

File tree

3 files changed

+196
-86
lines changed

3 files changed

+196
-86
lines changed

crates/djls-template-ast/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ toml = "0.8"
1212

1313
[dev-dependencies]
1414
insta = { version = "1.41", features = ["yaml"] }
15+
tempfile = "3.8"

crates/djls-template-ast/src/parser.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::ast::{Ast, AstError, Block, DjangoFilter, LineOffsets, Node, Span, Tag};
2-
use crate::tagspecs::{TagSpec, TagType};
2+
use crate::tagspecs::{TagType, TagSpecs};
33
use crate::tokens::{Token, TokenStream, TokenType};
44
use thiserror::Error;
55

@@ -103,7 +103,7 @@ impl Parser {
103103
assignment: None,
104104
};
105105

106-
let specs = TagSpec::load_builtin_specs()?;
106+
let specs = TagSpecs::load_builtin_specs()?;
107107
let spec = match specs.get(&tag_name) {
108108
Some(spec) => spec,
109109
None => return Ok(Node::Block(Block::Tag { tag })),
Lines changed: 193 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,112 @@
1-
use anyhow::{Context, Result};
1+
use anyhow::Result;
22
use serde::Deserialize;
33
use std::collections::HashMap;
4+
use std::convert::TryFrom;
45
use std::fs;
56
use std::ops::{Deref, Index};
67
use std::path::Path;
8+
use thiserror::Error;
79
use toml::Value;
810

9-
#[derive(Debug, Default)]
11+
#[derive(Debug, Error)]
12+
pub enum TagSpecError {
13+
#[error("Failed to read file: {0}")]
14+
Io(#[from] std::io::Error),
15+
#[error("Failed to parse TOML: {0}")]
16+
Toml(#[from] toml::de::Error),
17+
#[error("Failed to extract specs: {0}")]
18+
Extract(String),
19+
#[error(transparent)]
20+
Other(#[from] anyhow::Error),
21+
}
22+
23+
#[derive(Clone, Debug, Default)]
1024
pub struct TagSpecs(HashMap<String, TagSpec>);
1125

1226
impl TagSpecs {
1327
pub fn get(&self, key: &str) -> Option<&TagSpec> {
1428
self.0.get(key)
1529
}
16-
}
1730

18-
impl From<&Path> for TagSpecs {
19-
fn from(specs_dir: &Path) -> Self {
20-
let mut specs = HashMap::new();
31+
/// Load specs from a TOML file, looking under the specified table path
32+
fn load_from_toml(path: &Path, table_path: &[&str]) -> Result<Self, anyhow::Error> {
33+
let content = fs::read_to_string(path)?;
34+
let value: Value = toml::from_str(&content)?;
35+
36+
// Navigate to the specified table
37+
let table = table_path
38+
.iter()
39+
.try_fold(&value, |current, &key| {
40+
current
41+
.get(key)
42+
.ok_or_else(|| anyhow::anyhow!("Missing table: {}", key))
43+
})
44+
.unwrap_or(&value);
2145

22-
for entry in fs::read_dir(specs_dir).expect("Failed to read specs directory") {
23-
let entry = entry.expect("Failed to read directory entry");
24-
let path = entry.path();
25-
26-
if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
27-
let content = fs::read_to_string(&path).expect("Failed to read spec file");
28-
29-
let value: Value = toml::from_str(&content).expect("Failed to parse TOML");
46+
let mut specs = HashMap::new();
47+
TagSpec::extract_specs(table, None, &mut specs)
48+
.map_err(|e| TagSpecError::Extract(e.to_string()))?;
49+
Ok(TagSpecs(specs))
50+
}
3051

31-
TagSpec::extract_specs(&value, None, &mut specs).expect("Failed to extract specs");
52+
/// Load specs from a user's project directory
53+
pub fn load_user_specs(project_root: &Path) -> Result<Self, anyhow::Error> {
54+
// List of config files to try, in priority order
55+
let config_files = ["djls.toml", ".djls.toml", "pyproject.toml"];
56+
57+
for &file in &config_files {
58+
let path = project_root.join(file);
59+
if path.exists() {
60+
return match file {
61+
"pyproject.toml" => {
62+
Self::load_from_toml(&path, &["tool", "djls", "template", "tags"])
63+
}
64+
_ => Self::load_from_toml(&path, &[]), // Root level for other files
65+
};
3266
}
3367
}
34-
35-
TagSpecs(specs)
68+
Ok(Self::default())
3669
}
37-
}
38-
39-
impl Deref for TagSpecs {
40-
type Target = HashMap<String, TagSpec>;
4170

42-
fn deref(&self) -> &Self::Target {
43-
&self.0
44-
}
45-
}
71+
/// Load builtin specs from the crate's tagspecs directory
72+
pub fn load_builtin_specs() -> Result<Self, anyhow::Error> {
73+
let specs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tagspecs");
74+
let mut specs = HashMap::new();
4675

47-
impl IntoIterator for TagSpecs {
48-
type Item = (String, TagSpec);
49-
type IntoIter = std::collections::hash_map::IntoIter<String, TagSpec>;
76+
for entry in fs::read_dir(&specs_dir)? {
77+
let entry = entry?;
78+
let path = entry.path();
79+
if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
80+
let file_specs = Self::load_from_toml(&path, &[])?;
81+
specs.extend(file_specs.0);
82+
}
83+
}
5084

51-
fn into_iter(self) -> Self::IntoIter {
52-
self.0.into_iter()
85+
Ok(TagSpecs(specs))
5386
}
54-
}
5587

56-
impl<'a> IntoIterator for &'a TagSpecs {
57-
type Item = (&'a String, &'a TagSpec);
58-
type IntoIter = std::collections::hash_map::Iter<'a, String, TagSpec>;
88+
/// Merge another TagSpecs into this one, with the other taking precedence
89+
pub fn merge(&mut self, other: TagSpecs) -> &mut Self {
90+
self.0.extend(other.0);
91+
self
92+
}
5993

60-
fn into_iter(self) -> Self::IntoIter {
61-
self.0.iter()
94+
/// Load both builtin and user specs, with user specs taking precedence
95+
pub fn load_all(project_root: &Path) -> Result<Self, anyhow::Error> {
96+
let mut specs = Self::load_builtin_specs()?;
97+
let user_specs = Self::load_user_specs(project_root)?;
98+
Ok(specs.merge(user_specs).clone())
6299
}
63100
}
64101

65-
impl Index<&str> for TagSpecs {
66-
type Output = TagSpec;
102+
impl TryFrom<&Path> for TagSpecs {
103+
type Error = TagSpecError;
67104

68-
fn index(&self, index: &str) -> &Self::Output {
69-
&self.0[index]
105+
fn try_from(path: &Path) -> Result<Self, Self::Error> {
106+
Self::load_from_toml(path, &[]).map_err(Into::into)
70107
}
71108
}
72109

73-
impl AsRef<HashMap<String, TagSpec>> for TagSpecs {
74-
fn as_ref(&self) -> &HashMap<String, TagSpec> {
75-
&self.0
76-
}
77-
}
78110
#[derive(Debug, Clone, Deserialize)]
79111
pub struct TagSpec {
80112
#[serde(rename = "type")]
@@ -86,16 +118,11 @@ pub struct TagSpec {
86118
}
87119

88120
impl TagSpec {
89-
pub fn load_builtin_specs() -> Result<TagSpecs> {
90-
let specs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tagspecs");
91-
Ok(TagSpecs::from(specs_dir.as_path()))
92-
}
93-
94121
fn extract_specs(
95122
value: &Value,
96123
prefix: Option<&str>,
97124
specs: &mut HashMap<String, TagSpec>,
98-
) -> Result<()> {
125+
) -> Result<(), String> {
99126
// Try to deserialize as a tag spec first
100127
match TagSpec::deserialize(value.clone()) {
101128
Ok(tag_spec) => {
@@ -159,63 +186,145 @@ mod tests {
159186
use super::*;
160187

161188
#[test]
162-
fn test_specs_are_valid() -> Result<()> {
163-
let specs = TagSpec::load_builtin_specs()?;
189+
fn test_specs_are_valid() -> Result<(), anyhow::Error> {
190+
let specs = TagSpecs::load_builtin_specs()?;
164191

165192
assert!(!specs.0.is_empty(), "Should have loaded at least one spec");
166193

167-
println!("Loaded {} tag specs:", specs.0.len());
168194
for (name, spec) in &specs.0 {
169-
println!(" {} ({:?})", name, spec.tag_type);
195+
assert!(!name.is_empty(), "Tag name should not be empty");
196+
assert!(
197+
spec.tag_type == TagType::Block || spec.tag_type == TagType::Variable,
198+
"Tag type should be block or variable"
199+
);
170200
}
171-
172201
Ok(())
173202
}
174203

175204
#[test]
176-
fn test_builtin_django_tags() -> Result<()> {
177-
let specs = TagSpec::load_builtin_specs()?;
205+
fn test_builtin_django_tags() -> Result<(), anyhow::Error> {
206+
let specs = TagSpecs::load_builtin_specs()?;
178207

179-
// Test using Index trait
180-
let if_tag = &specs["if"];
208+
// Test using get method
209+
let if_tag = specs.get("if").expect("if tag should be present");
181210
assert_eq!(if_tag.tag_type, TagType::Block);
182-
assert_eq!(if_tag.closing, Some("endif".to_string()));
183-
184-
let if_branches = if_tag
211+
assert_eq!(if_tag.closing.as_deref(), Some("endif"));
212+
assert_eq!(if_tag.branches.as_ref().map(|b| b.len()), Some(2));
213+
assert!(if_tag
185214
.branches
186215
.as_ref()
187-
.expect("if tag should have branches");
188-
assert!(if_branches.iter().any(|b| b == "elif"));
189-
assert!(if_branches.iter().any(|b| b == "else"));
216+
.unwrap()
217+
.contains(&"elif".to_string()));
218+
assert!(if_tag
219+
.branches
220+
.as_ref()
221+
.unwrap()
222+
.contains(&"else".to_string()));
190223

191-
// Test using get method
192224
let for_tag = specs.get("for").expect("for tag should be present");
193225
assert_eq!(for_tag.tag_type, TagType::Block);
194-
assert_eq!(for_tag.closing, Some("endfor".to_string()));
195-
196-
let for_branches = for_tag
226+
assert_eq!(for_tag.closing.as_deref(), Some("endfor"));
227+
assert_eq!(for_tag.branches.as_ref().map(|b| b.len()), Some(1));
228+
assert!(for_tag
197229
.branches
198230
.as_ref()
199-
.expect("for tag should have branches");
200-
assert!(for_branches.iter().any(|b| b == "empty"));
231+
.unwrap()
232+
.contains(&"empty".to_string()));
201233

202-
// Test using HashMap method directly via Deref
203234
let block_tag = specs.get("block").expect("block tag should be present");
204235
assert_eq!(block_tag.tag_type, TagType::Block);
205-
assert_eq!(block_tag.closing, Some("endblock".to_string()));
236+
assert_eq!(block_tag.closing.as_deref(), Some("endblock"));
206237

207-
// Test iteration
208-
let mut count = 0;
209-
for (name, spec) in &specs {
210-
println!("Found tag: {} ({:?})", name, spec.tag_type);
211-
count += 1;
212-
}
213-
assert!(count > 0, "Should have found some tags");
238+
Ok(())
239+
}
240+
241+
#[test]
242+
fn test_user_defined_tags() -> Result<(), anyhow::Error> {
243+
// Create a temporary directory for our test project
244+
let dir = tempfile::tempdir()?;
245+
let root = dir.path();
246+
247+
// Create a pyproject.toml with custom tags
248+
let pyproject_content = r#"
249+
[tool.djls.template.tags.mytag]
250+
type = "block"
251+
closing = "endmytag"
252+
branches = ["mybranch"]
253+
args = [{ name = "myarg", required = true }]
254+
"#;
255+
fs::write(root.join("pyproject.toml"), pyproject_content)?;
256+
257+
// Load both builtin and user specs
258+
let specs = TagSpecs::load_all(root)?;
259+
260+
// Check that builtin tags are still there
261+
let if_tag = specs.get("if").expect("if tag should be present");
262+
assert_eq!(if_tag.tag_type, TagType::Block);
263+
264+
// Check our custom tag
265+
let my_tag = specs.get("mytag").expect("mytag should be present");
266+
assert_eq!(my_tag.tag_type, TagType::Block);
267+
assert_eq!(my_tag.closing, Some("endmytag".to_string()));
268+
269+
let branches = my_tag
270+
.branches
271+
.as_ref()
272+
.expect("mytag should have branches");
273+
assert!(branches.iter().any(|b| b == "mybranch"));
274+
275+
let args = my_tag.args.as_ref().expect("mytag should have args");
276+
let arg = &args[0];
277+
assert_eq!(arg.name, "myarg");
278+
assert!(arg.required);
214279

215-
// Test as_ref
216-
let map_ref: &HashMap<_, _> = specs.as_ref();
217-
assert_eq!(map_ref.len(), count);
280+
// Clean up temp dir
281+
dir.close()?;
282+
Ok(())
283+
}
218284

285+
#[test]
286+
fn test_config_file_priority() -> Result<(), anyhow::Error> {
287+
// Create a temporary directory for our test project
288+
let dir = tempfile::tempdir()?;
289+
let root = dir.path();
290+
291+
// Create all config files with different tags
292+
let djls_content = r#"
293+
[mytag1]
294+
type = "block"
295+
closing = "endmytag1"
296+
"#;
297+
fs::write(root.join("djls.toml"), djls_content)?;
298+
299+
let pyproject_content = r#"
300+
[tool.djls.template.tags]
301+
mytag2.type = "block"
302+
mytag2.closing = "endmytag2"
303+
"#;
304+
fs::write(root.join("pyproject.toml"), pyproject_content)?;
305+
306+
// Load user specs
307+
let specs = TagSpecs::load_user_specs(root)?;
308+
309+
// Should only have mytag1 since djls.toml has highest priority
310+
assert!(specs.get("mytag1").is_some(), "mytag1 should be present");
311+
assert!(
312+
specs.get("mytag2").is_none(),
313+
"mytag2 should not be present"
314+
);
315+
316+
// Remove djls.toml and try again
317+
fs::remove_file(root.join("djls.toml"))?;
318+
let specs = TagSpecs::load_user_specs(root)?;
319+
320+
// Should now have mytag2 since pyproject.toml has second priority
321+
assert!(
322+
specs.get("mytag1").is_none(),
323+
"mytag1 should not be present"
324+
);
325+
assert!(specs.get("mytag2").is_some(), "mytag2 should be present");
326+
327+
dir.close()?;
219328
Ok(())
220329
}
221330
}

0 commit comments

Comments
 (0)