SkillAgentSearch skills...

Tableshape

Test the shape or structure of a Lua table, inspired by React.PropTypes & LPeg

Install / Use

/learn @leafo/Tableshape
About this skill

Quality Score

0/100

Supported Platforms

Universal

Tags

README

tableshape

test

A Lua library for verifying the shape (schema, structure, etc.) of a table, and transforming it if necessary. The type checking syntax is inspired by the PropTypes module of React. Complex types & value transformations can be expressed using an operator overloading syntax similar to LPeg.

Install

$ luarocks install tableshape

Quick usage

local types = require("tableshape").types

-- define the shape of our player object
local player_shape = types.shape{
  class = types.one_of{"player", "enemy"},
  name = types.string,
  position = types.shape{
    x = types.number,
    y = types.number,
  },
  inventory = types.array_of(types.shape{
    name = types.string,
    id = types.integer
  }):is_optional()
}



-- create a valid object to test the shape with
local player = {
  class = "player",
  name = "Lee",
  position = {
    x = 2.8,
    y = 8.5
  },
}

-- verify that it matches the shape
assert(player_shape(player))

-- let's break the shape to see the error message:
player.position.x = "heck"
assert(player_shape(player))

-- error: field `position`: field `x`: got type `string`, expected `number`

Transforming

A malformed value can be repaired to the expected shape by using the transformation operator and method. The input value is cloned and modified before being returned.

local types = require("tableshape").types

-- a type checker that will coerce a value into a number from a string or return 0
local number = types.number + types.string / tonumber + types.any / 0

number:transform(5) --> 5
number:transform("500") --> 500
number:transform("hi") --> 0
number:transform({}) --> 0

Because type checkers are composable objects, we can build more complex types out of existing types we've written:


-- here we reference our transforming number type from above
local coordinate = types.shape {
  x = number,
  y = number
}

-- a compound type checker that can fill in missing values
local player_shape = types.shape({
  name = types.string + types.any / "unknown",
  position = coordinate
})

local bad_player = {
  position = {
    x = "234",
    y = false
  }
}

local fixed_player = player_shape:transform(bad_player)

-- fixed_player --> {
--   name = "unknown",
--   position = {
--     x = 234,
--     y = 0
--   }
-- }

Tutorial

To load the library require it. The most important part of the library is the types table, which will give you acess to all the type checkers

local types = require("tableshape").types

You can use the types table to check the types of simple values, not just tables. Calling the type checker like a function will test a value to see if it matches the shape or type. It returns true on a match, or nil and the error message if it fails. (This is done with the __call metamethod, you can also use the check_value method directly)

types.string("hello!") --> true
types.string(777)      --> nil, expected type "string", got "number"

You can see the full list of the available types below in the reference.

The real power of tableshape comes from the ability to describe complex types by nesting the type checkers.

Here we test for an array of numbers by using array_of:

local numbers_shape = types.array_of(types.number)

assert(numbers_shape({1,2,3}))

-- error: item 2 in array does not match: got type `string`, expected `number`
assert(numbers_shape({1,"oops",3}))

Note: The type checking is strict, a string that looks like a number, "123", is not a number and will trigger an error!

The structure of a generic table can be tested with types.shape. It takes a mapping table where the key is the field to check, and the value is the type checker:

local object_shape = types.shape{
  id = types.number,
  name = types.string:is_optional(),
}

-- success
assert(object_shape({
  id = 1234,
  name = "hello world"
}))

-- sucess, optional field is not there
assert(object_shape({
  id = 1235,
}))


-- error: field `id`: got type `nil`, expected `number`
assert(object_shape({
  name = 424,
}))

The is_optional method can be called on any type checker to return a new type checker that can also accept nil as a value. (It is equivalent to t + types['nil'])

If multiple fields fail the type check in a shape, the error message will contain all the failing fields

You can also use a literal value to match it directly: (This is equivalent to using types.literal(v))

local object_shape = types.shape{
  name = "Cowcat"
}

-- error: field `name` expected `Cowcat`
assert(object_shape({
  name = "Cowdog"
}))

The one_of type constructor lets you specify a list of types, and will succeed if one of them matches. (It works the same as the + operator)

local func_or_bool = types.one_of { types.func, types.boolean }

assert(func_or_bool(function() end))

-- error: expected type "function", or type "boolean"
assert(func_or_bool(2345))

It can also be used with literal values as well:

local limbs = types.one_of{"foot", "arm"}

assert(limbs("foot")) -- success
assert(limbs("arm")) -- success

-- error: expected "foot", or "arm"
assert(limbs("baseball"))

The pattern type can be used to test a string with a Lua pattern

local no_spaces = types.pattern "^[^%s]*$"

assert(no_spaces("hello!"))

-- error: doesn't match pattern `^[^%s]*$`
assert(no_spaces("oh no!"))

These examples only demonstrate some of the type checkers provided. You can see all the other type checkers in the reference below.

Type operators

Type checker objects have the operators *, +, and / overloaded to provide a quick way to make composite types.

  • * — The all of (and) operator, both operands must match.
  • + — The first of (or) operator, the operands are checked against the value from left to right
  • / — The transform operator, when using the transform method, the value will be converted by what's to the right of the operator
  • % — The transform with state operator, same as transform, but state is passed as second argument

The 'all of' operator

The all of operator checks if a value matches multiple types. Types are checked from left to right, and type checking will abort on the first failed check. It works the same as types.all_of.

local s = types.pattern("^hello") * types.pattern("world$")

s("hello 777 world")   --> true
s("good work")         --> nil, "doesn't match pattern `^hello`"
s("hello, umm worldz") --> nil, "doesn't match pattern `world$`"

The 'first of' operator

The first of operator checks if a value matches one of many types. Types are checked from left to right, and type checking will succeed on the first matched type. It works the same as types.one_of.

Once a type has been matched, no additional types are checked. If you use a greedy type first, like types.any, then it will not check any additional ones. This is important to realize if your subsequent types have any side effects like transformations or tags.

local s = types.number + types.string

s(44)            --> true
s("hello world") --> true
s(true)          --> nil, "no matching option (got type `boolean`, expected `number`; got type `boolean`, expected `string`)"

The 'transform' operator

In type matching mode, the transform operator has no effect. When using the transform method, however, the value will be modified by a callback or changed to a fixed value.

The following syntax is used: type / transform_callback --> transformable_type

local t = types.string + types.any / "unknown"

The proceeding type can be read as: "Match any string, or for any other type, transform it into the string 'unknown'".

t:transform("hello") --> "hello"
t:transform(5)       --> "unknown"

Because this type checker uses types.any, it will pass for whatever value is handed to it. A transforming type can fail also fail, here's an example:

local n = types.number + types.string / tonumber

n:transform("5") --> 5
n:transform({})  --> nil, "no matching option (got type `table`, expected `number`; got type `table`, expected `string`)"

The transform callback can either be a function, or a literal value. If a function is used, then the function is called with the current value being transformed, and the result of the transformation should be returned. If a literal value is used, then the transformation always turns the value into the specified value.

A transform function is not a predicate, and can't directly cause the type checking to fail. Returning nil is valid and will change the value to nil. If you wish to fail based on a function you can use the custom type or chain another type checker after the transformation:

-- this will fail unless `tonumber` returns a number
local t = (types.string / tonumber) * types.number
t:transform("nothing") --> nil, "got type `nil`, expected `number`"

A common pattern for repairing objects involves testing for the types you know how to fix followed by + types.any, followed by a type check of the final type you want:

Here we attempt to repair a value to the expected format for an x,y coordinate:

local types = require("tableshape").types

local str_to_coord = types.string / function(str)
  local x,y = str:match("(%d+)[^%d]+(%d+)")
  if not x then return end
  return {
    x = tonumber(x),
    y = tonumber(y)
  }
end

local array_to_coord = types.shape{types.number, types.number} / function(a)
  return {
    x = a[1],
    y = a[2]
  }
end

local cord = (str_to_coord + array_to_coord + types.any) * types.shape {
  x = types.number,
  y = types.number
}

cord:transform("100,200")        --> { x = 100, y = 200}
c
View on GitHub
GitHub Stars119
CategoryDevelopment
Updated11d ago
Forks10

Languages

MoonScript

Security Score

80/100

Audited on Mar 19, 2026

No findings