Skip to content

Commit 84fca57

Browse files
bordeuxclaude
andcommitted
refactor: migrate hash functions to filter_functions (Phase 2)
Implement unified filter-function pattern for hash functions: - Create filter_functions/hash.rs with Md5, Sha1, Sha256, Sha512 - Implement FilterFunction trait for all hash functions - Register as both functions AND filters - Remove old functions/hash.rs module - Update unit tests for both syntaxes - Add integration tests for filter syntax and chaining Both syntaxes now work identically: - Function: {{ sha256(string="hello") }} - Filter: {{ "hello" | sha256 }} - Chaining: {{ "hello" | sha256 | md5 }} 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9ef556d commit 84fca57

6 files changed

Lines changed: 539 additions & 111 deletions

File tree

src/filter_functions/hash.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//! Hash functions that support both function and filter syntax.
2+
//!
3+
//! # Function Syntax
4+
//! ```jinja
5+
//! {{ md5(string="hello") }}
6+
//! {{ sha256(string="hello world") }}
7+
//! ```
8+
//!
9+
//! # Filter Syntax
10+
//! ```jinja
11+
//! {{ "hello" | md5 }}
12+
//! {{ "hello world" | sha256 }}
13+
//! ```
14+
//!
15+
//! # Chaining
16+
//! ```jinja
17+
//! {{ "hello" | sha256 | md5 }}
18+
//! ```
19+
20+
use super::FilterFunction;
21+
use minijinja::value::Kwargs;
22+
use minijinja::{Error, ErrorKind, Value};
23+
24+
/// MD5 hash function.
25+
///
26+
/// # Function Syntax
27+
/// ```jinja
28+
/// {{ md5(string="hello") }}
29+
/// ```
30+
///
31+
/// # Filter Syntax
32+
/// ```jinja
33+
/// {{ "hello" | md5 }}
34+
/// ```
35+
pub struct Md5;
36+
37+
impl Md5 {
38+
fn hash(input: &str) -> String {
39+
use md5::{Digest, Md5 as Md5Hasher};
40+
let mut hasher = Md5Hasher::new();
41+
hasher.update(input.as_bytes());
42+
format!("{:x}", hasher.finalize())
43+
}
44+
}
45+
46+
impl FilterFunction for Md5 {
47+
const NAME: &'static str = "md5";
48+
49+
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
50+
let input: String = kwargs.get("string")?;
51+
Ok(Value::from(Self::hash(&input)))
52+
}
53+
54+
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
55+
let input = value
56+
.as_str()
57+
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "md5 requires a string"))?;
58+
Ok(Value::from(Self::hash(input)))
59+
}
60+
}
61+
62+
/// SHA1 hash function.
63+
///
64+
/// # Function Syntax
65+
/// ```jinja
66+
/// {{ sha1(string="hello") }}
67+
/// ```
68+
///
69+
/// # Filter Syntax
70+
/// ```jinja
71+
/// {{ "hello" | sha1 }}
72+
/// ```
73+
pub struct Sha1;
74+
75+
impl Sha1 {
76+
fn hash(input: &str) -> String {
77+
use sha1::{Digest, Sha1 as Sha1Hasher};
78+
let mut hasher = Sha1Hasher::new();
79+
hasher.update(input.as_bytes());
80+
format!("{:x}", hasher.finalize())
81+
}
82+
}
83+
84+
impl FilterFunction for Sha1 {
85+
const NAME: &'static str = "sha1";
86+
87+
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
88+
let input: String = kwargs.get("string")?;
89+
Ok(Value::from(Self::hash(&input)))
90+
}
91+
92+
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
93+
let input = value
94+
.as_str()
95+
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "sha1 requires a string"))?;
96+
Ok(Value::from(Self::hash(input)))
97+
}
98+
}
99+
100+
/// SHA256 hash function.
101+
///
102+
/// # Function Syntax
103+
/// ```jinja
104+
/// {{ sha256(string="hello") }}
105+
/// ```
106+
///
107+
/// # Filter Syntax
108+
/// ```jinja
109+
/// {{ "hello" | sha256 }}
110+
/// ```
111+
pub struct Sha256;
112+
113+
impl Sha256 {
114+
fn hash(input: &str) -> String {
115+
use sha2::{Digest, Sha256 as Sha256Hasher};
116+
let mut hasher = Sha256Hasher::new();
117+
hasher.update(input.as_bytes());
118+
format!("{:x}", hasher.finalize())
119+
}
120+
}
121+
122+
impl FilterFunction for Sha256 {
123+
const NAME: &'static str = "sha256";
124+
125+
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
126+
let input: String = kwargs.get("string")?;
127+
Ok(Value::from(Self::hash(&input)))
128+
}
129+
130+
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
131+
let input = value
132+
.as_str()
133+
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "sha256 requires a string"))?;
134+
Ok(Value::from(Self::hash(input)))
135+
}
136+
}
137+
138+
/// SHA512 hash function.
139+
///
140+
/// # Function Syntax
141+
/// ```jinja
142+
/// {{ sha512(string="hello") }}
143+
/// ```
144+
///
145+
/// # Filter Syntax
146+
/// ```jinja
147+
/// {{ "hello" | sha512 }}
148+
/// ```
149+
pub struct Sha512;
150+
151+
impl Sha512 {
152+
fn hash(input: &str) -> String {
153+
use sha2::{Digest, Sha512 as Sha512Hasher};
154+
let mut hasher = Sha512Hasher::new();
155+
hasher.update(input.as_bytes());
156+
format!("{:x}", hasher.finalize())
157+
}
158+
}
159+
160+
impl FilterFunction for Sha512 {
161+
const NAME: &'static str = "sha512";
162+
163+
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
164+
let input: String = kwargs.get("string")?;
165+
Ok(Value::from(Self::hash(&input)))
166+
}
167+
168+
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
169+
let input = value
170+
.as_str()
171+
.ok_or_else(|| Error::new(ErrorKind::InvalidOperation, "sha512 requires a string"))?;
172+
Ok(Value::from(Self::hash(input)))
173+
}
174+
}

src/filter_functions/mod.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
//! {{ "hello" | sha256 | base64_encode }}
2727
//! ```
2828
29+
pub mod hash;
2930
pub mod traits;
3031

3132
pub use traits::FilterFunction;
@@ -46,12 +47,12 @@ use minijinja::Environment;
4647
/// let mut env = Environment::new();
4748
/// filter_functions::register_all(&mut env);
4849
/// ```
49-
pub fn register_all(_env: &mut Environment) {
50+
pub fn register_all(env: &mut Environment) {
5051
// Phase 2: Hash functions
51-
// hash::Md5::register(env);
52-
// hash::Sha1::register(env);
53-
// hash::Sha256::register(env);
54-
// hash::Sha512::register(env);
52+
hash::Md5::register(env);
53+
hash::Sha1::register(env);
54+
hash::Sha256::register(env);
55+
hash::Sha512::register(env);
5556

5657
// Phase 3: Encoding functions
5758
// encoding::Base64Encode::register(env);

src/functions/hash.rs

Lines changed: 0 additions & 85 deletions
This file was deleted.

src/functions/mod.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
//!
1515
//! tmpltool provides additional custom functions:
1616
//! - `filter_env(pattern)` - Filter environment variables by glob pattern (e.g., "SERVER_*")
17-
//! - `md5(string)` - Calculate MD5 hash of a string
18-
//! - `sha1(string)` - Calculate SHA1 hash of a string
19-
//! - `sha256(string)` - Calculate SHA256 hash of a string
20-
//! - `sha512(string)` - Calculate SHA512 hash of a string
2117
//! - `uuid(version)` - Generate a UUID (v4 default, or v7 for time-ordered)
2218
//! - `random_string(length, charset)` - Generate a random string with custom length and character set
2319
//! - `read_file(path)` - Read content from a file
@@ -77,7 +73,6 @@ pub mod encoding;
7773
pub mod environment;
7874
pub mod exec;
7975
pub mod filesystem;
80-
pub mod hash;
8176
pub mod kubernetes;
8277
pub mod logic;
8378
pub mod math;
@@ -140,10 +135,7 @@ pub fn register_all(env: &mut Environment, context: TemplateContext) {
140135

141136
// Register custom functions (simple, no context needed)
142137
env.add_function("filter_env", environment::filter_env_fn);
143-
env.add_function("md5", hash::md5_fn);
144-
env.add_function("sha1", hash::sha1_fn);
145-
env.add_function("sha256", hash::sha256_fn);
146-
env.add_function("sha512", hash::sha512_fn);
138+
// Hash functions (md5, sha1, sha256, sha512) are now registered via filter_functions module
147139
env.add_function("uuid", uuid_gen::uuid_fn);
148140
env.add_function("random_string", random::random_string_fn);
149141

0 commit comments

Comments
 (0)