Skip to content

House rules in YAML

apiverity validate openapi.yaml --policy-file house-style.yaml
apiverity rules --policy-vocabulary

Rule packs are the right answer for a rule with real logic in it. They are the wrong answer for "every path must be kebab-case" — which is most of what an organisation actually wants to enforce, and does not justify a Python package, a release process and somewhere to publish it.

pack: acme-house-style
version: 1.0.0
rules:
  - id: ACME-PATHS-KEBAB
    severity: warn
    rationale: a mixed-case path is a support ticket about a 404
    remediation: rename it, adding the old path as a deprecated alias first
    select: operation
    where:
      path: {matches: '^(/[a-z0-9-]+|/\{[a-zA-Z0-9_]+\})+$'}

Put a regular expression in single quotes. YAML processes escapes inside double quotes, so "\{" is a parse error before this file ever sees it.

A fixed vocabulary, not an expression language

Every selector reads one field of the normalized contract model. There is no eval, no JSONPath, no embedded language, and that ceiling is deliberate.

A general expression evaluator would be a second engine inside this one — the same objection the Spectral importer makes — and it would let somebody write a rule this project cannot explain, in a tool whose whole claim is that every finding has a stable id and a reason.

When the vocabulary runs out, write a pack. It is twenty lines of Python.

apiverity rules --policy-vocabulary prints the whole vocabulary, from the same tables the loader validates against — so what the documentation says is available cannot disagree with what is accepted.

Selectors

operation one finding per operation that fails (the default)
service one finding for the contract

Fields

operation: operation_id, method, path, summary, description, tags, security, responses, parameters, request_body.

service: title, version, servers, security_schemes, global_security.

Predicates

present / absent the field is set, or is not
matches / not_matches every value matches this regex, or none does
includes / excludes the collection contains this value, or does not
one_of every value is in this list

matches on a field that is not there fails. absent is the predicate for asking about that, and conflating the two would make matches silently pass on every operation that omits the field.

An unknown word fails the run

The failure this is built to avoid: a DSL that accepts an unrecognised selector, matches nothing, and hands a team a gate they believe they have.

error: rule 'ACME-PATHS' selects `pathh`, which is not a field this vocabulary
has. Available: description, method, operation_id, parameters, path, …

At load time, before anything has been checked and reported clean. A regex that will not compile is refused there too, rather than at check time against the first contract somebody runs it on.

Exemptions name what they exempt

    except:
      - "POST /users"

A list of operation keys, not a pattern. A glob that grows to cover six operations nobody reviewed is the escape hatch becoming the policy.

For an exemption with an owner, a reason and an expiry, use suppressions instead — that is what they are for.

Scoping

protocols: [openapi, graphql] restricts a rule to the protocols it makes sense for. A protocol that does not exist is refused by name.

What the artifact records

A run with a policy file names it:

{ "command": "validate", "policies": ["house-style.yaml"], "findings": [...] }

Two runs over the same contract reporting different findings should say why.