1- use anyhow:: { Context , Result } ;
1+ use anyhow:: Result ;
22use serde:: Deserialize ;
33use std:: collections:: HashMap ;
4+ use std:: convert:: TryFrom ;
45use std:: fs;
56use std:: ops:: { Deref , Index } ;
67use std:: path:: Path ;
8+ use thiserror:: Error ;
79use 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 ) ]
1024pub struct TagSpecs ( HashMap < String , TagSpec > ) ;
1125
1226impl 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 ) ]
79111pub struct TagSpec {
80112 #[ serde( rename = "type" ) ]
@@ -86,16 +118,11 @@ pub struct TagSpec {
86118}
87119
88120impl 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