Skip to content

Type Middleware

arc is based on a middleware design pattern. This also applies to arc's type system to give you further flexibility in defining your APIs.

What is a Type Middleware?

A Type Middleware is simply a callable object that recieves a value, and then returns a value of the same type. It can be used to modify that value, validate some property of that value, or simply analyze the value and return it.

Middlewares fall into a couple of rough categories, and so arc uses specific terms to refer to them.

  • Validator: A middleware that checks the value against some condition. It raises an exception if the condition is not met, and otherwise it returns the original value
  • Transformer: A middleware that modifies the value and returns it.
  • Observer: A middleware that just analyzes the type, but won't every raise an error / manipulate it

These terms will be used throughout the rest of this page.

Creating a Type Middleware

Because middleware are just callables, they are extremely simple to define. For example, we could create a transform middleware that rounds floats to 2 digits.

def round2(val: float):
    return round(val, 2)

arc already ships with a Round() transformer, so you wouldn't actually need to implement this one yourself.

Using a Type Middleware

Type Middleware are attached to a type using an Annotated Type

from typing import Annotated
import arc

# Could use arc.types.middleware.Round() instead
# of implementing a custom middleware.
def round2(val: float):
    return round(val, 2)

def command(val: Annotated[float, round2]):

$ python 1.123456789

Middlewares are actually why most custom arc types require you to use arc.convert() to convert them properly. A good majority of them are actually just an underlying type + some middleware to provide a validation / conversion step. For example arc.types.PositiveInt is actually just defined as

PositiveInt = Annotated[int, GreaterThan(0)]

Builtin Middlewares

arc ships with a set of general-use builtin middlewares


Middleware that ensure that a passed in version is greater than the current version

import typing as t
import arc
from arc import types

def greater_than_previous(value: types.SemVer, ctx: arc.Context):
    current_version: types.SemVer | None = ctx.state.get("curr_version")
    if not current_version:
        return value

    if current_version >= value:
        raise arc.ValidationError(
            f"New version must be greater than current version ({current_version})"

    return value

NewVersion = t.Annotated[types.SemVer, greater_than_previous]

def command(version: NewVersion):

command(state={"curr_version": types.SemVer.parse("1.0.0")})
$ python 0.1.1
USAGE [-h] version

invalid value for version: New version must be greater than current version (1.0.0)
$ python 1.2.1