microspec

A micro specification parser and data validator for TCP protocols. The protocol can be defined either inside the code or as a standalone JSON which can be loaded via Protocol.from_dict(...). Then, all the data payloads for all the defined endpoints are simply validated via protocol.validate_endpoint(endpoint, payload) -> ValidationError | None.

Usage:

  • Via pip: pip install microspec
  • From source code:
git clone https://gitlab.com/meehai/microspec                 # clone the source code
cd microspec                                                  # go in the cloned directory
python -m venv .venv && source .venv/bin/activate             # make a virtual env, optional but useful
python -m pip install -e .                                    # install microspec in this virtual env
python -m pytest test/                                        # run the unit & integration tests to verify installation
python microspec/microspec.py test/integration/protocol.json  # smoke run: parse + validate the bundled spec

Docs: meehai.gitlab.io/microspec — built by docs/build_docs.sh (pdoc; no sphinx/config). Build locally with bash docs/build_docs.sh and open the printed file:// link.

Usage

Protocol:

{
  "move": {
    "input":  {"control_input": {"dtype": "float32", "shape": [6], "range": [-100, 100]}},
    "output": {"status": {"dtype": "str_enum", "enum": ["move_applied"]}, "new_state": {"dtype": "dict"}}
  }
}
import json
from microspec import Protocol

# Can also be defined here manually via the `Endpoint`, `Field` classes and `Dtype` enum from the library.
protocol = Protocol.from_dict(json.load(open("test/integration/protocol.json")), n_max_robots=10)

err = protocol.validate_endpoint("move", {"control_input": [5, 5, 5, 3, 3, 3]})
if err is not None: # err is of type ValidationError (has .error, .endpoint, .field for context)
  raise ValueError(f"payload is not valid: {err.endpoint}: {err.error}")

Spec format

Each command is an input / output map of name -> field. Errors are not per-command: every endpoint shares one error shape, declared once as Protocol's error_field (default Field("error", Dtype.STR)).

{
  "move": {
    "input":  {"control_input": {"dtype": "float32", "shape": [6], "range": [-100, 100]}},
    "output": {"status": {"dtype": "str_enum", "enum": ["move_applied"]}, "new_state": {"dtype": "dict"}}
  },
  "robot_get_state": {
    "input":  {"robot_ix": {"dtype": "int32", "range": [0, "${n_max_robots}"]}},
    "output": {"robot": {"dtype": "dict"}}
  }
}

Field schema

key applies to meaning
dtype required str int32 float32 bool dict bytes str_enum int_enum
shape array dtypes e.g. [6]; null = free first axis ([null, 6])
range int32 / float32 [min, max], inclusive (NaN always rejected; ±Inf only if outside the range)
enum str_enum / int_enum non-empty list of allowed values (required for enum dtypes)
min_len arrays with free axis minimum length of the null axis
fields dict optional nested name -> field map; omit for an opaque dict

Notes: a shape key means "array" (numpy, exact dtype — np.float32, not int64); scalars have no shape. A dict with fields is validated recursively (keys must match exactly, nested arrays are list→ndarray converted); without fields it is opaque (any dict passes). ${var} is a single bare variable filled at parse time (e.g. n_max_robots). The spec file is plain JSON.

Public API

Protocol, Endpoint, Field, Dtype, ValidationError. Everything else is internal.