Skip to content

Commit 6c0ae42

Browse files
bordeuxclaude
andcommitted
feat: enhance build_url with default scheme and object query support
Improvements to build_url function: 1. Default scheme: Defaults to "https" if scheme parameter is not provided - Users can still override with scheme="http" or any other scheme - Makes the API more convenient for common HTTPS use cases 2. Query parameter now accepts both strings and objects: - String: Works as before, passed through directly - Object: Automatically serialized to query string - Cleaner syntax: build_url(host="api.com", query={"page": 1}) Examples: - {{ build_url(host="example.com") }} → https://example.com/ - {{ build_url(scheme="http", host="localhost") }} → http://localhost/ - {{ build_url(host="api.com", query="page=1&limit=20") }} - {{ build_url(host="api.com", query={"page": 1, "limit": 20}) }} Tests: - Added 3 new unit tests for default scheme and object queries - Updated integration tests with new test cases - All 34 unit tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 13a455a commit 6c0ae42

3 files changed

Lines changed: 105 additions & 16 deletions

File tree

src/functions/url.rs

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ pub fn parse_url_fn(kwargs: Kwargs) -> Result<Value, Error> {
105105
///
106106
/// # Arguments
107107
///
108-
/// * `scheme` - The URL scheme (http, https, etc.)
109-
/// * `host` - The hostname
108+
/// * `scheme` - Optional URL scheme (default: "https")
109+
/// * `host` - The hostname (required)
110110
/// * `port` - Optional port number
111111
/// * `path` - Optional path component (default: "/")
112-
/// * `query` - Optional query string (without ?)
112+
/// * `query` - Optional query string (string) or object (will be serialized)
113113
///
114114
/// # Returns
115115
///
@@ -118,14 +118,15 @@ pub fn parse_url_fn(kwargs: Kwargs) -> Result<Value, Error> {
118118
/// # Example
119119
///
120120
/// ```jinja
121-
/// {{ build_url(scheme="https", host="api.example.com", port=8080, path="/v1/users", query="limit=10") }}
121+
/// {{ build_url(host="api.example.com", port=8080, path="/v1/users", query="limit=10") }}
122+
/// {{ build_url(host="api.example.com", query={"page": 1, "limit": 20}) }}
122123
/// ```
123124
pub fn build_url_fn(kwargs: Kwargs) -> Result<Value, Error> {
124-
let scheme: String = kwargs.get("scheme")?;
125+
let scheme: String = kwargs.get("scheme").unwrap_or_else(|_| "https".to_string());
125126
let host: String = kwargs.get("host")?;
126127
let port: Option<u16> = kwargs.get("port").ok();
127128
let path: Option<String> = kwargs.get("path").ok();
128-
let query: Option<String> = kwargs.get("query").ok();
129+
let query: Option<Value> = kwargs.get("query").ok();
129130

130131
// Start with scheme and host
131132
let mut url = format!("{}://{}", scheme, host);
@@ -143,11 +144,48 @@ pub fn build_url_fn(kwargs: Kwargs) -> Result<Value, Error> {
143144
url.push_str(&path_str);
144145

145146
// Add query string if specified
146-
if let Some(q) = query
147-
&& !q.is_empty()
148-
{
149-
url.push('?');
150-
url.push_str(&q);
147+
if let Some(q) = query {
148+
let query_str = if let Some(s) = q.as_str() {
149+
// Query is a string, use it directly
150+
s.to_string()
151+
} else {
152+
// Query is an object, serialize it using query_string logic
153+
let json_value: serde_json::Value = serde_json::to_value(&q).map_err(|e| {
154+
Error::new(
155+
ErrorKind::InvalidOperation,
156+
format!("Failed to convert query parameter: {}", e),
157+
)
158+
})?;
159+
160+
if !json_value.is_object() {
161+
return Err(Error::new(
162+
ErrorKind::InvalidOperation,
163+
"query parameter must be a string or object",
164+
));
165+
}
166+
167+
let mut parts = Vec::new();
168+
if let Some(obj) = json_value.as_object() {
169+
for (key, value) in obj {
170+
let encoded_key = urlencoding::encode(key);
171+
let value_str = match value {
172+
serde_json::Value::String(s) => s.clone(),
173+
serde_json::Value::Number(n) => n.to_string(),
174+
serde_json::Value::Bool(b) => b.to_string(),
175+
serde_json::Value::Null => String::from("null"),
176+
_ => value.to_string(),
177+
};
178+
let encoded_value = urlencoding::encode(&value_str);
179+
parts.push(format!("{}={}", encoded_key, encoded_value));
180+
}
181+
}
182+
parts.join("&")
183+
};
184+
185+
if !query_str.is_empty() {
186+
url.push('?');
187+
url.push_str(&query_str);
188+
}
151189
}
152190

153191
Ok(Value::from(url))

tests/integration/tests/22_url_functions.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,16 @@ assert_equals "$OUTPUT" "pass" "parse_url extracts password"
7575
# build_url Tests
7676
# ============================================================================
7777

78-
# Test 13: Build simple URL
78+
# Test 13: Build simple URL with explicit scheme
7979
create_template "build_url_simple.tmpl" '{{ build_url(scheme="https", host="example.com") }}'
8080
OUTPUT=$(run_binary "build_url_simple.tmpl")
8181
assert_equals "$OUTPUT" "https://example.com/" "build_url creates simple URL"
8282

83+
# Test 13b: Build simple URL with default scheme (https)
84+
create_template "build_url_default_scheme.tmpl" '{{ build_url(host="example.com") }}'
85+
OUTPUT=$(run_binary "build_url_default_scheme.tmpl")
86+
assert_equals "$OUTPUT" "https://example.com/" "build_url uses https as default scheme"
87+
8388
# Test 14: Build URL with port
8489
create_template "build_url_port.tmpl" '{{ build_url(scheme="https", host="example.com", port=8080) }}'
8590
OUTPUT=$(run_binary "build_url_port.tmpl")
@@ -154,11 +159,18 @@ assert_equals "$OUTPUT" "" "query_string returns empty string for empty object"
154159
# Combined use cases
155160
# ============================================================================
156161

157-
# Test 26: Build URL with query_string
162+
# Test 26: Build URL with query_string function
158163
create_template "combined_build_query.tmpl" '{% set params = {"page": 1, "limit": 10} %}{{ build_url(scheme="https", host="api.example.com", path="/users", query=query_string(params=params)) }}'
159164
OUTPUT=$(run_binary "combined_build_query.tmpl")
160165
assert_contains "$OUTPUT" "https://api.example.com/users?" "combined build_url with query_string"
161166

167+
# Test 26b: Build URL with query object directly
168+
create_template "build_url_query_object.tmpl" '{% set params = {"page": 1, "limit": 10} %}{{ build_url(host="api.example.com", path="/users", query=params) }}'
169+
OUTPUT=$(run_binary "build_url_query_object.tmpl")
170+
assert_contains "$OUTPUT" "https://api.example.com/users?" "build_url accepts query as object"
171+
assert_contains "$OUTPUT" "page=1" "build_url query object includes page"
172+
assert_contains "$OUTPUT" "limit=10" "build_url query object includes limit"
173+
162174
# Test 27: Parse and rebuild URL
163175
create_template "combined_parse_build.tmpl" '{% set original = parse_url(url="https://example.com:8080/api") %}{{ build_url(scheme=original.scheme, host=original.host, port=original.port, path=original.path) }}'
164176
OUTPUT=$(run_binary "combined_parse_build.tmpl")

tests/test_url_functions.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,14 @@ fn test_build_url_http_scheme() {
346346
}
347347

348348
#[test]
349-
fn test_build_url_missing_scheme() {
349+
fn test_build_url_default_scheme() {
350350
let result = url::build_url_fn(Kwargs::from_iter(vec![(
351351
"host",
352352
Value::from("example.com"),
353-
)]));
353+
)]))
354+
.unwrap();
354355

355-
assert!(result.is_err());
356+
assert_eq!(result.to_string(), "https://example.com/");
356357
}
357358

358359
#[test]
@@ -362,6 +363,44 @@ fn test_build_url_missing_host() {
362363
assert!(result.is_err());
363364
}
364365

366+
#[test]
367+
fn test_build_url_with_query_object() {
368+
let mut params = BTreeMap::new();
369+
params.insert("page".to_string(), Value::from(1));
370+
params.insert("limit".to_string(), Value::from(20));
371+
372+
let result = url::build_url_fn(Kwargs::from_iter(vec![
373+
("host", Value::from("api.example.com")),
374+
("path", Value::from("/users")),
375+
("query", Value::from_object(params)),
376+
]))
377+
.unwrap();
378+
379+
let output = result.to_string();
380+
assert!(output.starts_with("https://api.example.com/users?"));
381+
assert!(output.contains("page=1"));
382+
assert!(output.contains("limit=20"));
383+
}
384+
385+
#[test]
386+
fn test_build_url_with_query_object_complex() {
387+
let mut params = BTreeMap::new();
388+
params.insert("search".to_string(), Value::from("hello world"));
389+
params.insert("active".to_string(), Value::from(true));
390+
params.insert("count".to_string(), Value::from(42));
391+
392+
let result = url::build_url_fn(Kwargs::from_iter(vec![
393+
("host", Value::from("example.com")),
394+
("query", Value::from_object(params)),
395+
]))
396+
.unwrap();
397+
398+
let output = result.to_string();
399+
assert!(output.contains("active=true"));
400+
assert!(output.contains("count=42"));
401+
assert!(output.contains("search=hello") || output.contains("search=hello%20world"));
402+
}
403+
365404
// ============================================================================
366405
// query_string Tests
367406
// ============================================================================

0 commit comments

Comments
 (0)