]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] Add callable defaults support to lua_shape
authorVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 17 Nov 2025 21:31:55 +0000 (21:31 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Mon, 17 Nov 2025 21:31:55 +0000 (21:31 +0000)
Enhance lua_shape to support function values as defaults that are
evaluated dynamically at validation time, not at schema definition time.

Core changes:
- check_optional(): Check if default is a function and call it
- check_table(): Same for table field defaults
- Enables patterns like: T.string():with_default(get_timestamp)

lua_aws.lua cleanup:
Replace ugly patterns like:
  T.transform(T.one_of({T.string(), T.literal(nil)}),
    function(v) return v or 'GET' end)
With clean:
  T.string():with_default('GET')

For dynamic defaults (date field), use:
  T.string():with_default(today_canonical)  -- function ref, not call

Benefits:
- Much cleaner and more readable schemas
- Consistent with lua_shape design philosophy
- Dynamic defaults for timestamps, random values, etc.
- Static defaults for constants

All tests pass (44/44).

lualib/lua_aws.lua
lualib/lua_shape/README.md
lualib/lua_shape/core.lua

index 7d81477ba0bb70844458f3e415e4104bb5a3b12d..3e50e094107ed135913447419fd34a0d94a0c1e3 100644 (file)
@@ -170,25 +170,13 @@ end
 exports.aws_canon_request_hash = aws_canon_request_hash
 
 local aws_authorization_hdr_args_schema = T.table({
-  date = T.transform(
-    T.one_of({T.string(), T.literal(nil)}),
-    function(v) return v or today_canonical() end
-  ),
+  date = T.string():with_default(today_canonical),
   secret_key = T.string(),
-  method = T.transform(
-    T.one_of({T.string(), T.literal(nil)}),
-    function(v) return v or 'GET' end
-  ),
+  method = T.string():with_default('GET'),
   uri = T.string(),
   region = T.string(),
-  service = T.transform(
-    T.one_of({T.string(), T.literal(nil)}),
-    function(v) return v or 's3' end
-  ),
-  req_type = T.transform(
-    T.one_of({T.string(), T.literal(nil)}),
-    function(v) return v or 'aws4_request' end
-  ),
+  service = T.string():with_default('s3'),
+  req_type = T.string():with_default('aws4_request'),
   -- headers is a table with string keys and string values (map_of equivalent)
   headers = T.table({}, { open = true, extra = T.string() }),
   key_id = T.string(),
index 016bdf32bb38ac9c9e89408e9db4794d85a38284..32fcf732a8d4d2da2132460e6688ef18dd9949f0 100644 (file)
@@ -72,7 +72,7 @@ local ok, config = config_schema:transform({
 ### Composition
 
 - `schema:optional()` - Make schema optional
-- `schema:with_default(value)` - Add default value
+- `schema:with_default(value)` - Add default value (can be a function for dynamic defaults)
 - `schema:doc(doc_table)` - Add documentation
 - `schema:transform_with(fn)` - Apply transformation
 - `T.transform(schema, fn)` - Transform wrapper
@@ -167,6 +167,26 @@ local timeout_schema = T.transform(T.number({ min = 0 }), function(val)
 end)
 ```
 
+### Callable Defaults
+
+Defaults can be functions that are called each time a default is needed:
+
+```lua
+local function get_current_timestamp()
+  return os.time()
+end
+
+local event_schema = T.table({
+  name = T.string(),
+  timestamp = T.number():with_default(get_current_timestamp),  -- Function called each time
+  priority = T.integer():with_default(0)  -- Static default
+})
+
+-- Each transform gets a fresh timestamp
+local ok, event1 = event_schema:transform({ name = "login" })
+-- event1.timestamp will be the current time when transform was called
+```
+
 ### Schema Registry
 
 ```lua
index c8b353a4749b60a2a80ba6ba7158abc916b2e382..977cd9691ee55543fc892a4cbfe00f408120231b 100644 (file)
@@ -414,7 +414,12 @@ local function check_table(node, value, ctx)
       if field_spec.optional then
         -- Apply default in transform mode
         if ctx.mode == "transform" and field_spec.default ~= nil then
-          result[field_name] = field_spec.default
+          local default_val = field_spec.default
+          -- Support callable defaults: if default is a function, call it
+          if type(default_val) == "function" then
+            default_val = default_val()
+          end
+          result[field_name] = default_val
         end
       else
         has_errors = true
@@ -513,7 +518,12 @@ end
 local function check_optional(node, value, ctx)
   if value == nil then
     if ctx.mode == "transform" and node.default ~= nil then
-      return true, node.default
+      local default_val = node.default
+      -- Support callable defaults: if default is a function, call it
+      if type(default_val) == "function" then
+        default_val = default_val()
+      end
+      return true, default_val
     end
     return true, nil
   end