Skip to content

Commit c437928

Browse files
Move tagspec loading to djls-conf, add compile-time builtins (#222)
1 parent 6c44e6d commit c437928

File tree

14 files changed

+1507
-662
lines changed

14 files changed

+1507
-662
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
File renamed without changes.

crates/djls-conf/src/lib.rs

Lines changed: 243 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
pub mod tagspecs;
2+
13
use std::fs;
24
use std::path::Path;
35

@@ -9,6 +11,13 @@ use directories::ProjectDirs;
911
use serde::Deserialize;
1012
use thiserror::Error;
1113

14+
pub use crate::tagspecs::ArgTypeDef;
15+
pub use crate::tagspecs::EndTagDef;
16+
pub use crate::tagspecs::IntermediateTagDef;
17+
pub use crate::tagspecs::SimpleArgTypeDef;
18+
pub use crate::tagspecs::TagArgDef;
19+
pub use crate::tagspecs::TagSpecDef;
20+
1221
#[derive(Error, Debug)]
1322
pub enum ConfigError {
1423
#[error("Configuration build/deserialize error")]
@@ -26,6 +35,8 @@ pub struct Settings {
2635
#[serde(default)]
2736
debug: bool,
2837
venv_path: Option<String>,
38+
#[serde(default)]
39+
tagspecs: Vec<TagSpecDef>,
2940
}
3041

3142
impl Settings {
@@ -88,6 +99,11 @@ impl Settings {
8899
pub fn venv_path(&self) -> Option<&str> {
89100
self.venv_path.as_deref()
90101
}
102+
103+
#[must_use]
104+
pub fn tagspecs(&self) -> &[TagSpecDef] {
105+
&self.tagspecs
106+
}
91107
}
92108

93109
#[cfg(test)]
@@ -110,7 +126,8 @@ mod tests {
110126
settings,
111127
Settings {
112128
debug: false,
113-
venv_path: None
129+
venv_path: None,
130+
tagspecs: vec![],
114131
}
115132
);
116133
}
@@ -346,4 +363,229 @@ mod tests {
346363
assert!(matches!(result.unwrap_err(), ConfigError::Config(_)));
347364
}
348365
}
366+
367+
mod tagspecs {
368+
use super::*;
369+
use crate::tagspecs::ArgTypeDef;
370+
use crate::tagspecs::SimpleArgTypeDef;
371+
372+
#[test]
373+
fn test_load_tagspecs_from_djls_toml() {
374+
let dir = tempdir().unwrap();
375+
let content = r#"
376+
[[tagspecs]]
377+
name = "mytag"
378+
module = "myapp.templatetags.custom"
379+
end_tag = { name = "endmytag" }
380+
381+
[[tagspecs]]
382+
name = "for"
383+
module = "django.template.defaulttags"
384+
end_tag = { name = "endfor" }
385+
intermediate_tags = [{ name = "empty" }]
386+
args = [
387+
{ name = "item", type = "variable" },
388+
{ name = "in", type = "literal" },
389+
{ name = "items", type = "variable" }
390+
]
391+
"#;
392+
fs::write(dir.path().join("djls.toml"), content).unwrap();
393+
let settings = Settings::new(dir.path()).unwrap();
394+
395+
assert_eq!(settings.tagspecs().len(), 2);
396+
397+
let mytag = &settings.tagspecs()[0];
398+
assert_eq!(mytag.name, "mytag");
399+
assert_eq!(mytag.module, "myapp.templatetags.custom");
400+
assert_eq!(mytag.end_tag.as_ref().unwrap().name, "endmytag");
401+
402+
let for_tag = &settings.tagspecs()[1];
403+
assert_eq!(for_tag.name, "for");
404+
assert_eq!(for_tag.module, "django.template.defaulttags");
405+
assert_eq!(for_tag.intermediate_tags.len(), 1);
406+
assert_eq!(for_tag.args.len(), 3);
407+
}
408+
409+
#[test]
410+
fn test_load_tagspecs_from_pyproject() {
411+
let dir = tempdir().unwrap();
412+
let content = r#"
413+
[tool.djls]
414+
debug = true
415+
416+
[[tool.djls.tagspecs]]
417+
name = "cache"
418+
module = "django.templatetags.cache"
419+
end_tag = { name = "endcache", optional = false }
420+
args = [
421+
{ name = "expire_time", type = "variable" },
422+
{ name = "fragment_name", type = "string" }
423+
]
424+
"#;
425+
fs::write(dir.path().join("pyproject.toml"), content).unwrap();
426+
let settings = Settings::new(dir.path()).unwrap();
427+
428+
assert_eq!(settings.tagspecs().len(), 1);
429+
let cache = &settings.tagspecs()[0];
430+
assert_eq!(cache.name, "cache");
431+
assert_eq!(cache.module, "django.templatetags.cache");
432+
assert_eq!(cache.args.len(), 2);
433+
}
434+
435+
#[test]
436+
fn test_arg_types() {
437+
let dir = tempdir().unwrap();
438+
let content = r#"
439+
[[tagspecs]]
440+
name = "test"
441+
module = "test.module"
442+
args = [
443+
{ name = "simple", type = "variable" },
444+
{ name = "choice", type = { choice = ["on", "off"] } },
445+
{ name = "optional", required = false, type = "string" }
446+
]
447+
"#;
448+
fs::write(dir.path().join("djls.toml"), content).unwrap();
449+
let settings = Settings::new(dir.path()).unwrap();
450+
451+
let test = &settings.tagspecs()[0];
452+
assert_eq!(test.args.len(), 3);
453+
454+
// Check simple type
455+
assert!(matches!(
456+
test.args[0].arg_type,
457+
ArgTypeDef::Simple(SimpleArgTypeDef::Variable)
458+
));
459+
460+
// Check choice type
461+
if let ArgTypeDef::Choice { ref choice } = test.args[1].arg_type {
462+
assert_eq!(choice, &vec!["on".to_string(), "off".to_string()]);
463+
} else {
464+
panic!("Expected choice type");
465+
}
466+
467+
// Check optional arg
468+
assert!(!test.args[2].required);
469+
}
470+
471+
#[test]
472+
fn test_intermediate_tags() {
473+
let dir = tempdir().unwrap();
474+
let content = r#"
475+
[[tagspecs]]
476+
name = "if"
477+
module = "django.template.defaulttags"
478+
end_tag = { name = "endif" }
479+
intermediate_tags = [
480+
{ name = "elif" },
481+
{ name = "else" }
482+
]
483+
args = [
484+
{ name = "condition", type = "expression" }
485+
]
486+
"#;
487+
fs::write(dir.path().join("djls.toml"), content).unwrap();
488+
let settings = Settings::new(dir.path()).unwrap();
489+
490+
let if_tag = &settings.tagspecs()[0];
491+
assert_eq!(if_tag.name, "if");
492+
493+
assert_eq!(if_tag.intermediate_tags.len(), 2);
494+
assert_eq!(if_tag.intermediate_tags[0].name, "elif");
495+
assert_eq!(if_tag.intermediate_tags[1].name, "else");
496+
}
497+
498+
#[test]
499+
fn test_end_tag_with_args() {
500+
let dir = tempdir().unwrap();
501+
let content = r#"
502+
[[tagspecs]]
503+
name = "block"
504+
module = "django.template.defaulttags"
505+
end_tag = { name = "endblock", args = [{ name = "name", required = false, type = "variable" }] }
506+
args = [
507+
{ name = "name", type = "variable" }
508+
]
509+
"#;
510+
fs::write(dir.path().join("djls.toml"), content).unwrap();
511+
let settings = Settings::new(dir.path()).unwrap();
512+
513+
let block_tag = &settings.tagspecs()[0];
514+
assert_eq!(block_tag.name, "block");
515+
516+
let end_tag = block_tag.end_tag.as_ref().unwrap();
517+
assert_eq!(end_tag.name, "endblock");
518+
assert_eq!(end_tag.args.len(), 1);
519+
assert!(!end_tag.args[0].required);
520+
}
521+
522+
#[test]
523+
fn test_tagspecs_with_other_settings() {
524+
let dir = tempdir().unwrap();
525+
let content = r#"
526+
debug = true
527+
venv_path = "/path/to/venv"
528+
529+
[[tagspecs]]
530+
name = "custom"
531+
module = "myapp.tags"
532+
args = []
533+
"#;
534+
fs::write(dir.path().join("djls.toml"), content).unwrap();
535+
let settings = Settings::new(dir.path()).unwrap();
536+
537+
assert!(settings.debug());
538+
assert_eq!(settings.venv_path(), Some("/path/to/venv"));
539+
assert_eq!(settings.tagspecs().len(), 1);
540+
assert_eq!(settings.tagspecs()[0].name, "custom");
541+
}
542+
543+
#[test]
544+
fn test_all_arg_types() {
545+
let dir = tempdir().unwrap();
546+
let content = r#"
547+
[[tagspecs]]
548+
name = "test_all_types"
549+
module = "test.module"
550+
args = [
551+
{ name = "literal", type = "literal" },
552+
{ name = "variable", type = "variable" },
553+
{ name = "string", type = "string" },
554+
{ name = "expression", type = "expression" },
555+
{ name = "assignment", type = "assignment" },
556+
{ name = "varargs", type = "varargs" }
557+
]
558+
"#;
559+
fs::write(dir.path().join("djls.toml"), content).unwrap();
560+
let settings = Settings::new(dir.path()).unwrap();
561+
562+
let test = &settings.tagspecs()[0];
563+
assert_eq!(test.args.len(), 6);
564+
565+
assert!(matches!(
566+
test.args[0].arg_type,
567+
ArgTypeDef::Simple(SimpleArgTypeDef::Literal)
568+
));
569+
assert!(matches!(
570+
test.args[1].arg_type,
571+
ArgTypeDef::Simple(SimpleArgTypeDef::Variable)
572+
));
573+
assert!(matches!(
574+
test.args[2].arg_type,
575+
ArgTypeDef::Simple(SimpleArgTypeDef::String)
576+
));
577+
assert!(matches!(
578+
test.args[3].arg_type,
579+
ArgTypeDef::Simple(SimpleArgTypeDef::Expression)
580+
));
581+
assert!(matches!(
582+
test.args[4].arg_type,
583+
ArgTypeDef::Simple(SimpleArgTypeDef::Assignment)
584+
));
585+
assert!(matches!(
586+
test.args[5].arg_type,
587+
ArgTypeDef::Simple(SimpleArgTypeDef::VarArgs)
588+
));
589+
}
590+
}
349591
}

crates/djls-conf/src/tagspecs.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use serde::Deserialize;
2+
3+
/// A single tag specification
4+
#[derive(Debug, Clone, Deserialize, PartialEq)]
5+
pub struct TagSpecDef {
6+
/// Tag name (e.g., "for", "if", "cache")
7+
pub name: String,
8+
/// Module where this tag is defined (e.g., "django.template.defaulttags")
9+
pub module: String,
10+
/// Optional end tag specification
11+
#[serde(default)]
12+
pub end_tag: Option<EndTagDef>,
13+
/// Optional intermediate tags (e.g., "elif", "else" for "if" tag)
14+
#[serde(default)]
15+
pub intermediate_tags: Vec<IntermediateTagDef>,
16+
/// Tag arguments specification
17+
#[serde(default)]
18+
pub args: Vec<TagArgDef>,
19+
}
20+
21+
/// End tag specification
22+
#[derive(Debug, Clone, Deserialize, PartialEq)]
23+
pub struct EndTagDef {
24+
/// End tag name (e.g., "endfor", "endif")
25+
pub name: String,
26+
/// Whether the end tag is optional
27+
#[serde(default)]
28+
pub optional: bool,
29+
/// Optional arguments for the end tag
30+
#[serde(default)]
31+
pub args: Vec<TagArgDef>,
32+
}
33+
34+
/// Intermediate tag specification
35+
#[derive(Debug, Clone, Deserialize, PartialEq)]
36+
pub struct IntermediateTagDef {
37+
/// Intermediate tag name (e.g., "elif", "else")
38+
pub name: String,
39+
/// Optional arguments for the end tag
40+
#[serde(default)]
41+
pub args: Vec<TagArgDef>,
42+
}
43+
44+
/// Tag argument specification
45+
#[derive(Debug, Clone, Deserialize, PartialEq)]
46+
pub struct TagArgDef {
47+
/// Argument name
48+
pub name: String,
49+
/// Whether the argument is required
50+
#[serde(default = "default_true")]
51+
pub required: bool,
52+
/// Argument type
53+
#[serde(rename = "type")]
54+
pub arg_type: ArgTypeDef,
55+
}
56+
57+
/// Argument type specification
58+
#[derive(Debug, Clone, Deserialize, PartialEq)]
59+
#[serde(untagged)]
60+
pub enum ArgTypeDef {
61+
/// Simple type like "variable", "string", etc.
62+
Simple(SimpleArgTypeDef),
63+
/// Choice from a list of values
64+
Choice { choice: Vec<String> },
65+
}
66+
67+
/// Simple argument types
68+
#[derive(Debug, Clone, Deserialize, PartialEq)]
69+
#[serde(rename_all = "lowercase")]
70+
pub enum SimpleArgTypeDef {
71+
Literal,
72+
Variable,
73+
String,
74+
Expression,
75+
Assignment,
76+
VarArgs,
77+
}
78+
79+
fn default_true() -> bool {
80+
true
81+
}

0 commit comments

Comments
 (0)