File: README.md

package info (click to toggle)
rspamd 3.14.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 35,064 kB
  • sloc: ansic: 247,728; cpp: 107,741; javascript: 31,385; perl: 3,089; asm: 2,512; pascal: 1,625; python: 1,510; sh: 589; sql: 313; makefile: 195; xml: 74
file content (432 lines) | stat: -rw-r--r-- 12,736 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# lua_shape

A comprehensive schema validation and transformation library for Rspamd, designed to replace tableshape with improved error reporting, documentation generation, and export capabilities.

## Features

1. **Better Error Reporting**: Structured error trees with intersection analysis for `one_of` types
2. **Documentation Generation**: Extract structured documentation from schemas
3. **Type Constraints**: Numeric ranges, string lengths, patterns, and more
4. **First-class Mixins**: Field composition with origin tracking
5. **JSON Schema Export**: Export schemas for UCL validation
6. **Transform Support**: Immutable transformations with validation
7. **Pure Lua**: No dependencies on external modules (except optional lpeg for patterns)

## Quick Start

```lua
local T = require "lua_shape.core"

-- Define a schema
local config_schema = T.table({
  host = T.string({ min_len = 1 }),
  port = T.integer({ min = 1, max = 65535 }):with_default(8080),
  timeout = T.number({ min = 0 }):optional(),
  ssl = T.boolean():with_default(false)
})

-- Validate configuration
local ok, result = config_schema:check({
  host = "localhost",
  port = 3000
})

if not ok then
  print("Validation error:")
  print(T.format_error(result))
end

-- Transform with defaults applied
local ok, config = config_schema:transform({
  host = "example.com"
})
-- config.port == 8080 (default applied)
-- config.ssl == false (default applied)
```

## Core Types

### Scalars

- `T.string(opts)` - String with optional constraints
  - `min_len`, `max_len` - Length constraints
  - `pattern` - Lua pattern for validation (e.g., `"^%d+$"` for digits only)
  - `lpeg` - Optional lpeg pattern for complex parsing
- `T.number(opts)` - Number with optional range constraints (min, max)
  - Accepts both numbers and values convertible via `tonumber`
- `T.integer(opts)` - Integer (number with integer constraint)
- `T.boolean()` - Boolean value
- `T.callable()` - Function/callable value
- `T.enum(values)` - One of a fixed set of values
- `T.literal(value)` - Exact value match

### Structured Types

- `T.array(item_schema, opts)` - Array with item validation
  - Underlying table must be a dense, 1-indexed array (no sparse or string keys)
  - `min_items`, `max_items` - Size constraints
- `T.table(fields, opts)` - Table/object with field schemas
  - `open = true` - Allow additional fields not defined in schema
  - `open = false` (default) - Reject unknown fields
  - `extra = schema` - Schema for validating extra fields
  - `mixins` - Array of mixin schemas for composition (applied when the schema is resolved via the registry)
- `T.one_of(variants)` - Sum type (match exactly one alternative)

### Composition

- `schema:optional()` - Make schema optional
- `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
- `T.ref(id)` - Reference to registered schema
- `T.mixin(schema, opts)` - Mixin for table composition

## Examples

### Basic Types with Constraints

```lua
-- String with length constraint
local name_schema = T.string({ min_len = 3, max_len = 50 })

-- String with Lua pattern (validates format)
local email_schema = T.string({ pattern = "^[%w%.]+@[%w%.]+$" })
local ipv4_schema = T.string({ pattern = "^%d+%.%d+%.%d+%.%d+$" })

-- Integer with range
local age_schema = T.integer({ min = 0, max = 150 })

-- Enum
local level_schema = T.enum({"debug", "info", "warning", "error"})
```

### Arrays and Tables

```lua
-- Array of strings
local tags_schema = T.array(T.string())

-- Table with required and optional fields
local user_schema = T.table({
  name = T.string(),
  email = T.string(),
  age = T.integer():optional(),
  role = T.enum({"admin", "user"}):with_default("user")
})

-- Closed table (default): rejects unknown fields
local strict_config = T.table({
  host = T.string(),
  port = T.integer()
}, { open = false })

-- Open table: allows additional fields not in schema
local flexible_config = T.table({
  host = T.string(),
  port = T.integer()
}, { open = true })
-- Accepts: { host = "localhost", port = 8080, custom_field = "value" }
```

### one_of with Intersection

```lua
-- Multiple config variants
local config_schema = T.one_of({
  {
    name = "file_config",
    schema = T.table({
      type = T.literal("file"),
      path = T.string()
    })
  },
  {
    name = "redis_config",
    schema = T.table({
      type = T.literal("redis"),
      host = T.string(),
      port = T.integer():with_default(6379)
    })
  }
})

-- Error messages show intersection:
-- "all alternatives require: type (string)"
```

### Transforms

`T.transform(accepted_type, transformer)` validates input against `accepted_type`, then applies `transformer` function.

```lua
-- Accept string, convert to number
local num_from_string = T.transform(T.string(), tonumber)

-- Accept number or string, convert both to number
local flexible_number = T.one_of({
  T.number(),
  T.transform(T.string(), tonumber)
})

-- Accept string, parse time interval to number
local timeout_schema = T.one_of({
  T.number({ min = 0 }),
  T.transform(T.string(), parse_time_interval)  -- "5s" -> 5.0
})
```

**Semantics:**
1. Input is validated against accepted type (first argument)
2. If valid, transformer function is called with pcall
3. If transformer returns `nil` or errors, validation fails
4. Otherwise, result is accepted without type checking

> **Note:** Transform functions run only in `schema:transform(...)` mode. In `schema:check(...)` mode, only the input type is validated.

### 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
local Registry = require "lua_shape.registry"
local reg = Registry.global()

-- Define reusable schemas
local redis_schema = reg:define("redis.options", T.table({
  servers = T.array(T.string()),
  db = T.integer({ min = 0, max = 15 }):with_default(0)
}))

-- Reference in other schemas
local app_schema = T.table({
  cache = T.ref("redis.options")
})

-- Resolve references
local resolved = reg:resolve_schema(app_schema)
-- Validate/transforms should use the resolved schema so mixins/references are applied
local ok, cfg_or_err = resolved:transform({
  cache = {
    servers = {"redis:6379"}
  }
})
```

### Mixins with Origin Tracking

```lua
-- Base mixin
local redis_mixin = T.table({
  redis_host = T.string(),
  redis_port = T.integer():with_default(6379)
})

-- Use mixin in another schema
local plugin_schema = T.table({
  enabled = T.boolean(),
  plugin_option = T.string()
}, {
  mixins = {
    T.mixin(redis_mixin, { as = "redis" })
  }
})

-- Documentation will show:
-- Direct fields: enabled, plugin_option
-- Mixin "redis": redis_host, redis_port
```

Mixins are merged into the resulting table schema by `Registry:resolve_schema` (or `Registry:define`). Always validate against the resolved schema so that mixin fields participate in `:check` / `:transform` and emit proper documentation/JSON Schema output.

### JSON Schema Export

```lua
local jsonschema = require "lua_shape.jsonschema"

-- Export single schema
local json = jsonschema.from_schema(config_schema, {
  id = "https://rspamd.com/schema/config",
  title = "Application Config"
})

-- Export all schemas from registry
local all_schemas = jsonschema.export_registry(Registry.global())
```

### Documentation Generation

```lua
local docs = require "lua_shape.docs"

-- Generate documentation IR
local doc_tree = docs.for_schema(config_schema)

-- Render as markdown
local markdown_lines = docs.render_markdown(doc_tree.schema_doc)
for _, line in ipairs(markdown_lines) do
  print(line)
end
```

## Error Reporting

### Structured Errors

Errors are represented as trees:

```lua
{
  kind = "table_invalid",
  path = "config",
  details = {
    errors = {
      port = {
        kind = "constraint_violation",
        path = "config.port",
        details = { constraint = "max", expected = 65535, got = 99999 }
      }
    }
  }
}
```

### Human-Readable Formatting

```lua
local T = require "lua_shape.core"
print(T.format_error(error_tree))
```

Output:
```
table validation failed at config:
  constraint violation at config.port: max (expected: 65535, got: 99999)
```

### one_of Intersection Errors

When all variants of a one_of fail, the error shows common requirements:

```
value does not match any alternative at :
  all alternatives require:
    - name: string
    - type: string
  some alternatives also expect:
    - path: string (in file_config variant)
    - host: string (in redis_config variant)
  tried alternatives:
    - file_config: ...
    - redis_config: ...
```

## API Reference

### Core Module (`lua_shape.core`)

#### Type Constructors

- `T.string(opts?)` - String type
  - opts: `min_len`, `max_len`, `pattern`, `lpeg`, `doc`
- `T.number(opts?)` - Number type
  - opts: `min`, `max`, `doc`
- `T.integer(opts?)` - Integer type (number with integer=true)
  - opts: `min`, `max`, `doc`
- `T.boolean(opts?)` - Boolean type
- `T.enum(values, opts?)` - Enum type
- `T.literal(value, opts?)` - Literal value type
- `T.array(item_schema, opts?)` - Array type
  - opts: `min_items`, `max_items`, `doc`
- `T.table(fields, opts?)` - Table type
  - opts: `open`, `extra`, `mixins`, `doc`
- `T.one_of(variants, opts?)` - Sum type
- `T.optional(schema, opts?)` - Optional wrapper
- `T.default(schema, value)` - Default value wrapper
- `T.transform(accepted_type, transformer, opts?)` - Transform wrapper (validates input against accepted_type, then applies transformer)
- `T.ref(id, opts?)` - Schema reference placeholder (must be resolved via the registry before validation)
- `T.mixin(schema, opts?)` - Mixin definition

#### Schema Methods

- `schema:check(value, ctx?)` - Validate value
- `schema:transform(value, ctx?)` - Transform and validate (tableshape-compatible `(result)` / `(nil, err)` contract)
- `schema:optional(opts?)` - Make optional
- `schema:with_default(value)` - Add default
- `schema:doc(doc_table)` - Add documentation
- `schema:transform_with(fn, opts?)` - Add transformation

### Registry Module (`lua_shape.registry`)

- `Registry.global()` - Get/create global registry
- `registry:define(id, schema)` - Register schema with ID (returns the resolved version with mixins/reference chains applied)
- `registry:get(id)` - Get schema by ID
- `registry:resolve_schema(schema)` - Resolve references and mixins (recurses into nested arrays/one_of/options and caches the result)
- `registry:list()` - List all schema IDs
- `registry:export_all()` - Export all schemas

### Core Utilities

The core module also includes utility functions:

- `T.format_error(err)` - Format error tree as human-readable string
- `T.deep_clone(value)` - Deep clone value for immutable transformations

### JSON Schema Module (`lua_shape.jsonschema`)

- `jsonschema.from_schema(schema, opts?)` - Convert to JSON Schema
- `jsonschema.export_registry(registry, opts?)` - Export all schemas

### Docs Module (`lua_shape.docs`)

- `docs.for_schema(schema, opts?)` - Generate documentation IR
- `docs.for_registry(registry, opts?)` - Generate docs for all schemas
- `docs.render_markdown(doc_tree, indent?)` - Render as markdown

## Migration from tableshape

See [MIGRATION.md](MIGRATION.md) for detailed migration guide.

Quick reference:

| tableshape | lua_shape |
|------------|---------------|
| `ts.string` | `T.string()` |
| `ts.number` | `T.number()` |
| `ts.array_of(ts.string)` | `T.array(T.string())` |
| `ts.shape({...})` | `T.table({...})` |
| `field:is_optional()` | `field:optional()` or `{ schema = ..., optional = true }` |
| `ts.string + ts.number` | `T.one_of({ T.string(), T.number() })` |
| `ts.string / fn` | `T.string():transform_with(fn)` or `T.transform(T.string(), fn)` |
| `field:describe("...")` | `field:doc({ summary = "..." })` |

## Files

- `core.lua` - Core type system, validation, and utilities
- `registry.lua` - Schema registration and reference resolution
- `jsonschema.lua` - JSON Schema export
- `docs.lua` - Documentation generation
- `MIGRATION.md` - Migration guide from tableshape
- `README.md` - This file

## License

Apache License 2.0 - Same as Rspamd