Skip to content

Commit 004a25c

Browse files
committed
add initial implementation
1 parent 93424dc commit 004a25c

File tree

6 files changed

+443
-0
lines changed

6 files changed

+443
-0
lines changed

.luacheckrc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
return {
2+
read_globals = { "describe", "it" },
3+
4+
stds = {
5+
busted = {
6+
read_globals = {
7+
"describe",
8+
"it",
9+
"before_each",
10+
"after_each",
11+
"assert",
12+
"spy",
13+
},
14+
},
15+
},
16+
std = "_G+busted",
17+
}

.stylua.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
column_width = 120
2+
line_endings = "Unix"
3+
indent_type = "Spaces"
4+
indent_width = 4
5+
quote_style = "AutoPreferDouble"
6+
call_parentheses = "None"

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025 Christopher Kaster <[email protected]>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# spec.lua
2+
3+
spec.lua is a lightweight library for defining and validating data structures in Lua, inspired by Clojures `spec` system.
4+
5+
It allows you to declaratively specify the shape and constraints of your data (e.g. tables, strings, numbers) and validate
6+
against those specs at runtime. This helps catch errors early, document your data contracts and make your code more robust.
7+
8+
## Installation
9+
10+
The recommended way to install `spec.lua` is to put the file inside your project as a vendored library
11+
12+
## Example
13+
14+
```lua
15+
local spec = require "spec"
16+
17+
-- spec.lua provides built-in predicates for common types
18+
local is_string = spec.valid(spec.string, "Hello, World") -- true
19+
20+
assert(spec.valid(spec.number, 1337))
21+
22+
if spec.valid(spec.boolean, true) then
23+
-- ...
24+
end
25+
26+
-- create specs for table shapes
27+
local user_spec = spec.keys {
28+
name = spec.string,
29+
age = spec.number,
30+
}
31+
32+
spec.valid(user_spec, { name = "Alice", age = 30}) -- valid
33+
spec.valid(user_spec, { name = "Bob" }) -- invalid, missing key
34+
spec.valid(user_spec, { name = "Charlie", age = "thirty" }) -- invalid, not a number
35+
36+
-- or you can write your own predicates
37+
---@param my_type MyType
38+
---@return boolean
39+
function my_predicate(my_type)
40+
return my_type:condition_is_valid()
41+
end
42+
43+
spec.valid(my_predicate, instance_of_type) -- true if condition is valid
44+
45+
-- some useful helper functions...
46+
47+
-- conform returns the value if it conforms to the spec
48+
local alice = { name = "Alice", age = 30 }
49+
local bob = { name = "Bob" } -- invalid because it has no age...
50+
51+
local user = spec.conform(user_spec, alice) -- returns alice if valid
52+
if user then
53+
print(user.name) -- Alice
54+
end
55+
56+
spec.conform(user_spec, bob) -- returns nil
57+
58+
-- assert that the spec is valid otherwise throw an error
59+
spec.assert(spec.string, "Hello, World")
60+
```
61+
62+
## License
63+
64+
MIT

spec.lua

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
-- This file is part of spec.lua
2+
--
3+
-- Repository: https://github.com/atomicptr/spec.lua
4+
--
5+
-- License:
6+
--
7+
-- Copyright 2025 Christopher Kaster <[email protected]>
8+
--
9+
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
10+
-- documentation files (the “Software”), to deal in the Software without restriction, including without limitation
11+
-- the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
12+
-- and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
13+
--
14+
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions
15+
-- of the Software.
16+
--
17+
-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
18+
-- TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19+
-- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
20+
-- CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21+
-- DEALINGS IN THE SOFTWARE.
22+
23+
local M = {}
24+
25+
-- predicates
26+
27+
---Tests if value is a string
28+
---@param value any
29+
---@return boolean
30+
function M.string(value)
31+
return type(value) == "string"
32+
end
33+
34+
---Tests if value is a number
35+
---@param value any
36+
---@return boolean
37+
function M.number(value)
38+
return type(value) == "number"
39+
end
40+
41+
---Tests if value is a boolean
42+
---@param value any
43+
---@return boolean
44+
function M.boolean(value)
45+
return type(value) == "boolean"
46+
end
47+
48+
---Tests if value is a table
49+
---@param value any
50+
---@return boolean
51+
function M.table(value)
52+
return type(value) == "table"
53+
end
54+
55+
---Tests if value is nil
56+
---@param value any
57+
---@return boolean
58+
function M.null(value)
59+
return value == nil
60+
end
61+
62+
---Tests if value exists
63+
---@param value any
64+
---@return boolean
65+
function M.some(value)
66+
return value ~= nil
67+
end
68+
69+
---Test if table matches a shape
70+
---@param spec_map table<string, fun(value: any): boolean>
71+
---@return fun(value: any): boolean
72+
function M.keys(spec_map)
73+
return function(value)
74+
if not M.table(value) then
75+
return false
76+
end
77+
78+
for key, sub_spec in pairs(spec_map) do
79+
local val = value[key]
80+
81+
if val == nil or not (type(sub_spec) == "function" and sub_spec(val)) then
82+
return false
83+
end
84+
end
85+
86+
return true
87+
end
88+
end
89+
90+
---Check if a value matches with the given spec
91+
---@param spec fun(value: any): boolean
92+
---@param value any
93+
---@return boolean
94+
function M.valid(spec, value)
95+
if type(spec) ~= "function" then
96+
error "Spec must be a function (e.g. spec.keys for tables)"
97+
end
98+
99+
return spec(value)
100+
end
101+
102+
---Returns value if it matches the spec, nil otherwise
103+
---@generic T
104+
---@param spec fun(value: any): boolean
105+
---@param value T
106+
---@return T|nil
107+
function M.conform(spec, value)
108+
if M.valid(spec, value) then
109+
return value
110+
end
111+
112+
return nil
113+
end
114+
115+
---Asserts that a spec is valid
116+
---@param spec any
117+
---@param value any
118+
function M.assert(spec, value)
119+
assert(M.valid(spec, value))
120+
end
121+
122+
return M

0 commit comments

Comments
 (0)