metaty: typosafe types using Lua metatables

Metatype is a library and specification for creating performant, documented, and typo-safe Lua record-types which can be formatted.

The #metaty.freeze module allows making types immutable (after calling freeze on them) and making tables immutable.

Turn off typo checking by setting the global LUA_OPT=3 or higher.

Mod and Record

Below is a simple example using metaty.mod to create a module and calling metatype directly to create a record (the same as metaty.record).

local mty = require'metaty'

--- module documentation
local M = mty.mod'myMod'

--- Documentation for Pos (position)
M.Pos = mty'Pos' {
  -- fields are a string of form "name[type]: documentation",
  'x[int]: x coordinate',
  'y[int]: y coordinate', y = 0,
}

local p1 = Pos{x=4}
local p1 = Pos{x=4, y=3, z=5} -- ERROR: does not have field z

Enum Type

Metaty also supports creating typosafe enum types with metaty.enum. An enum is typosafe both when creating the variant (i.e. v = MyEnum.VARIANT) and when matching using the matcher method below. It also allows using checked enums in pod.html.

One of the main benefits of using an enum is to ensure that when you are matching you don't make a typo mistake (i.e. WOKER instead of WORKER). In lua there is no native switch statement (or similar), but table lookup on functions can be equally as good -- see the example below.

M.Job = enum'Job' {
  OWNER   = 1,
  MANAGER = 2,
  COOK    = 3,
  WAITER  = 4,
}

assert('OWNER', M.Job.OWNER)

-- string or id input returns string
assert('OWNER', M.Job.name(1))
assert('OWNER', M.Job.name('OWNER'))

-- string or id input returns id
assert(1, M.Job.id(1))
assert(1, M.Job.id('OWNER'))

-- create a table that converts a variant (name or id) -> function.
local doJob = M.Job:matcher {
  OWNER   = function() print'tell them to get to work' end,
  MANAGER = function() print'get to work!'             end,
  COOK    = function() print'order up!'                end,
  WAITER  = function() print'they want spam and eggs'  end,

  -- Removing any of the above will cause an error that not all variants
  -- are covered. You can use :partialMatcher if you want to
  -- return nil instead.
  --
  -- Below will cause an error: no variant DISHWASHER
  DISHWASHER = function() end
}

-- call in your own function like:
doJob[job](my, args)

Record Equivalent

The example in #record expands to the following. Note that the "typosafe" elements are removed when LUA_OPT > 3.
local M = {}
local metaty = require'metaty'

local Pos = setmetatable({
  __name='Pos',
  y = 0,
  -- used with metaty.Fmt and help()
  __fields={'x', 'y', x='[int]', y='[int]'},
  __newindex = metaty.newindex, -- typosafe setting
}, {
  __call = function(T, t)
    metaty.fieldsCheck(T.__fields, t) -- typosafe constructor
    return setmetatable(t, T)
  end,
  __index = metaty.index, -- typosafe getting
})
Pos.__index = Pos

-- `mod` gives documentation reflection
PKG_LOCS[M.myFn]         = 'path/to/file.lua:123'
PKG_NAMES[M.myFn]        = 'mymod.Pos'
PKG_LOOKUP['myMod.Pos'] = M.Pos

API

Why?

Lua is a fast and fun language. However it often lacks the ability to express intent when it comes to the structure of data. Also, not only is it not type-safe but it is also TYPO-unsafe -- small mistakes in the name of a field can easily result in hard to diagnose bugs, even when they occur in one's unit-test suite.

Checking for typos incurrs a small performance cost, so it is disabled by default. However, it is well-worth the cost in your unit tests.

Spec

For a type to be considered a "metaty" the only requirement is that it has a metatable set and that metatable has a __name field.

The following fields can optionally be set on the metatable:

In addition, there is runtime type specification defined below.

Runtime typo checking (optional)

Note: Runtime typo checking has a cost and can be disabled with LUA_OPT=3 or higher.

You can override the typo-checking behavior of a single type with

getmetatable(MyType).__call  = myConstructor
getmetatable(MyType).__index = myIndex
MyType.__newindex            = myNewIndex

LUA_OPT

LUA_OPT is an integer from 0-3 which is intended to have the following semantic meanings:

Mod metaty

metaty: simple but effective Lua type system using metatable

Functions

Mod metaty.freeze

Usage: freeze.freezy(MyType) to make MyType freezable, see

#metaty.freeze.freezy.

Call usage: freeze(v)

When called: freezes the value, see #metaty.freeze.freeze.

Types: frozen

Functions

Record frozen

A "plain old table" that has been frozen (made immutable).