-
Notifications
You must be signed in to change notification settings - Fork 1
Vacuum openapi #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zendern
wants to merge
35
commits into
main
Choose a base branch
from
vacuum
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Vacuum openapi #195
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
56684df
Integrate vacuum for open api spec validation
zendern 57737d3
Handle open api tags
zendern f0bd485
Add question to get rest api description
zendern c1139fb
Need the new param
zendern e340c76
FOrmatting
zendern 526e944
Updates to exception handler
zendern 152bc7d
Dont need it in devcontainers put it in pre-commit
zendern 37e9cc6
If has_backend aroudn pre-commit
zendern 7787b44
Correct commit
zendern fe149e8
dont need explicit definitions as we are just doing the defaults
zendern e0ae863
Cleanup
zendern b8bab3b
Drop report.html since its super generic
zendern 0d0861f
Remove kiota comment
zendern a6d5549
Bring back linting of template
zendern 827a518
fix(pre-commit): correct malformed raw tag in vacuum hook template
zendern 5855fcf
fix(backend): avoid constant redefinition in openapi tags setup
zendern 1acb235
fix(backend): generate openapi snapshot from real test instead of han…
zendern 707993b
fix(backend): add response model docstrings for vacuum component-desc…
zendern 955887a
fix(backend): replace placeholder graphql query with real system.vers…
zendern f5b26b2
Remove comment
zendern 07be353
chore(#195): add debug step to investigate pyright spawn failure
zendern 72d32ae
fix(#195): remove stale backend .venv after moving template out of ne…
zendern d0fb371
Remove DEBUG output
zendern bc16b3b
fix(backend): satisfy vacuum operation-tags and oas3-missing-example …
zendern 8f4e1a8
fix(backend): satisfy vacuum operation-tag-defined and oas3-missing-e…
zendern 87e0dc4
fix(backend): gate graphql-only vacuum ignore entries on backend_uses…
zendern a0e9f1f
Simplify description
zendern 2b636e9
Driver specific tags
zendern f92a309
feat(openapi): add API naming-convention vacuum rules
zendern 2c23abe
(feature) check for plural in paths
zendern 59cc3fa
fix(openapi): allowlist graphql path segment in plural check
zendern a0f290a
chore(pre-commit): show full violation details for vacuum lint
zendern 236deab
to json so quoting doesn't break things
zendern 131516e
fix(vacuum): enforce camelCase query params and repair casing checks
zendern 6405b2d
fix(backend): alias healthcheck prepend_v query param as prependV
zendern File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just | |
| context["python_faker_version"] = ">=40.28.1" | ||
| context["mutmut_version"] = ">=3.6.0" | ||
| context["pyrefly_version"] = ">=1.1.1" | ||
| context["vacuum_openapi_version"] = "0.29.9" | ||
|
zendern marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kill it |
||
|
|
||
| context["default_node_version"] = "24.11.1" | ||
| context["nuxt_ui_version"] = "^4.8.1" | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // Vacuum custom JS function (goja runtime: no npm, no require/import — plain self-contained JS). | ||
| // | ||
| // Flags static path segments that look like singular resource nouns; REST collections should be plural | ||
| // (gov.au naming). `given: $.paths[*]~` passes each path key (e.g. "/api/data/devices/{device_id}") as | ||
| // the single `input` argument. We split on "/", ignore "{param}" segments, and check each static segment. | ||
| // | ||
| // Pluralization is heuristic. To keep false positives down we DO NOT try to pluralize every word; instead | ||
| // we flag a segment only when it is clearly singular (does not end in "s") AND is not in the ALLOWLIST of | ||
| // non-collection segments (namespaces, versions, actions, status/singleton sub-resources, uncountable | ||
| // nouns). Projects can edit ALLOWLIST below — this file is copier-managed but locally customizable. | ||
|
|
||
| // Segments that are legitimately not plural collections. | ||
| var ALLOWLIST = [ | ||
| // namespace / versioning | ||
| "api", "v1", "v2", "v3", | ||
| // uncountable / mass nouns | ||
| "data", "series", "telemetry", "equipment", "info", "metadata", "media", "config", "configuration", | ||
| // health / status / lifecycle singletons | ||
| "health", "healthcheck", "healthz", "readyz", "livez", "status", "shutdown", "ping", "version", | ||
| // common actions, filters and singleton sub-resources | ||
| "me", "self", "search", "online", "offline", "push", "polling", "dashboard", "summary", "latest", "current", | ||
| // non-REST endpoints (not resource collections) | ||
| "graphql" | ||
| ]; | ||
|
|
||
| function getSchema() { | ||
| return { name: "resourcesPlural" }; | ||
| } | ||
|
|
||
| function isAllowed(seg) { | ||
| var lower = seg.toLowerCase(); | ||
| for (var i = 0; i < ALLOWLIST.length; i++) { | ||
| if (ALLOWLIST[i] === lower) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| // Lenient: anything ending in "s" is treated as already plural. This intentionally under-flags words like | ||
| // "status"/"analysis" (safe: those are allowlisted or uncountable) so we never wrongly flag a plural. | ||
| function looksPlural(seg) { | ||
| return seg.length > 1 && seg.charAt(seg.length - 1) === "s"; | ||
| } | ||
|
|
||
| function runRule(input) { | ||
| if (typeof input !== "string") { | ||
| return []; | ||
| } | ||
| var results = []; | ||
| var segments = input.split("/"); | ||
| for (var i = 0; i < segments.length; i++) { | ||
| var seg = segments[i]; | ||
| if (seg === "") { | ||
| continue; // leading slash / empty | ||
| } | ||
| if (seg.charAt(0) === "{") { | ||
| continue; // path parameter, not a resource segment | ||
| } | ||
| if (isAllowed(seg) || looksPlural(seg)) { | ||
| continue; | ||
| } | ||
| results.push({ | ||
| message: "path segment '" + seg + "' should be a plural resource noun (or added to the allowlist if it is not a collection)" | ||
| }); | ||
| } | ||
| return results; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| {% raw %}# Ignored vacuum findings, with justification. | ||
| # | ||
| # owasp-define-error-validation requires every operation to document a 400/422/4XX validation-error | ||
| # response. These five endpoints take no path/query/body parameters, so there is no honest client-error | ||
| # to document. The rule stays enabled so it enforces error documentation on all *future* endpoints; only | ||
| # these input-less operations are exempted. | ||
| owasp-define-error-validation: | ||
| - "$.paths['/api/shutdown'].get.responses" | ||
|
|
||
| # ValidationError is generated by FastAPI (the 422 body); its `loc` union contains an int we cannot | ||
| # annotate with a format/bounds without owning the model, so exempt that one generated node. | ||
| # HTTPValidationError is generated by FastAPI; its `detail` property is exempt because we cannot attach a | ||
| # field-level example to a generated model (a schema-level example is injected in custom_openapi instead).{% endraw %}{% if backend_uses_graphql %}{% raw %} | ||
| # /api/graphql's 200 response body is arbitrary JSON shaped by the caller's GraphQL query/GraphiQL page; | ||
| # there is no single representative example to attach without misleadingly implying a fixed response shape.{% endraw %}{% endif %}{% raw %} | ||
| oas3-missing-example: | ||
| - "$.components.schemas['HTTPValidationError'].properties['detail']"{% endraw %}{% if backend_uses_graphql %}{% raw %} | ||
| - "$.paths['/api/graphql'].get.responses['200'].content['application/json']" | ||
| - "$.paths['/api/graphql'].post.responses['200'].content['application/json']"{% endraw %}{% endif %}{% raw %} | ||
|
|
||
| owasp-integer-format: | ||
| - "$.components.schemas['ValidationError'].properties['loc'].items.anyOf[1]" | ||
| owasp-integer-limit: | ||
| - "$.components.schemas['ValidationError'].properties['loc'].items.anyOf[1]"{% endraw %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| # Curated vacuum ruleset for the backend OpenAPI schema. | ||
| # | ||
| # Extends vacuum's built-in OpenAPI "recommended" set, then disables two rules that are noise/false | ||
| # positives for a FastAPI-generated spec and enables a handful of opt-in best-practice rules plus three | ||
| # OWASP data-hardening rules. Every non-default rule carries a comment explaining WHY. | ||
| # | ||
| # The target document is the OpenAPI 3.1.0 snapshot the backend emits (also the canonical spec fed to | ||
| # Kiota codegen): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to comment about |
||
| # backend/tests/unit/__snapshots__/test_basic_server_functionality/test_openapi_schema.json | ||
| # | ||
| # `formats` is intentionally omitted from every custom rule below: this repo only ever lints one 3.1.0 | ||
| # document, so scoping by format would be dead configuration. | ||
| extends: | ||
| - - vacuum:oas | ||
| - recommended | ||
| rules: | ||
| # DISABLED: flags identical description strings, but ours repeat legitimately — the same field | ||
| # (e.g. SensorInfo.id) is inherited across many response models, and the same 404 text is reused | ||
| # across endpoints. Forcing unique wording would make the docs less consistent, not better. | ||
| description-duplication: "off" | ||
|
|
||
| # DISABLED: `servers` is meaningless here — single-server, air-gapped device, no multi-environment | ||
| # URLs to document. FastAPI's default "/ - This server" relative entry is all Swagger UI needs. | ||
| oas3-api-servers: "off" | ||
|
|
||
| # DISABLED: false positive — it does not follow `$ref`, so enum/model-typed properties | ||
| # (severity, direction, status, measurementType, etc.) that reference a fully-typed component are wrongly | ||
| # flagged as "missing type". Every code-level workaround trips a different recommended rule | ||
| # (allOf -> no-unnecessary-combinator; bare $ref -> oas3-missing-example and loses the field | ||
| # description), so disabling this single shallow rule is the cleanest option. The fields keep their | ||
| # normal Field(description=, examples=). A FastAPI-generated spec can otherwise only trigger this on | ||
| # `Any`-typed fields, which are rare and usually unavoidable (e.g. the generated ValidationError.input). | ||
| oas-missing-type: "off" | ||
|
|
||
| # ENABLED: guarantees every tag used by an operation is declared at the top level with real metadata — | ||
| # this drives navigation/grouping in docs and Kiota-generated client namespaces. | ||
| # `true` pulls vacuum's built-in definition verbatim (it's an opt-in rule, absent from `recommended`). | ||
| openapi-tags: true | ||
|
|
||
| # ENABLED: a tag with no description is useless to a consumer reading the docs — enforce a | ||
| # human-readable purpose for each of system/debug/devices/data/alarms. | ||
| tag-description: true | ||
|
|
||
| # ENABLED: keeps each operation to exactly one tag so it appears once in generated navigation/clients | ||
| # (multi-tagging duplicates operations across groups and confuses codegen). We already tag singly. | ||
| operation-singular-tag: true | ||
|
|
||
| # ENABLED: a POST with no documented success response is an incomplete contract; ensures every | ||
| # create/action endpoint declares its 2xx/3xx (we already return 201) so clients know the success shape. | ||
| post-response-success: true | ||
|
|
||
| # ENABLED: correctness guard — every entry in a schema's `required` list must actually exist in | ||
| # `properties`. Catches real bugs (typo'd/removed field left in required) that break client codegen. | ||
| required-fields-defined: true | ||
|
|
||
| # ENABLED (OWASP): three data-hardening rules worth keeping even on an air-gapped/no-auth API. The | ||
| # rest of the OWASP pack (auth/HTTPS/rate-limit/protection-*/string-restricted) is intentionally NOT | ||
| # enabled for this trusted air-gapped, no-auth deployment; revisit if the API is ever exposed/authed. | ||
| owasp-define-error-validation: true | ||
|
|
||
| # Integers must declare int32/int64 `format` so codegen picks the right numeric type. | ||
| # (Config, not `true`: OWASP rules aren't in the extended set, so `true` would be dropped.) | ||
| owasp-integer-format: true | ||
|
|
||
| # Integers must declare min/max bounds (input validation / resource-exhaustion guard). | ||
| owasp-integer-limit: true | ||
|
|
||
| # ENABLED: path id parameters must be explicit and resource-qualified, never a bare generic | ||
| # identifier. Bare `{id}` is ambiguous in a multi-resource spec and produces collision-prone Kiota | ||
| # client method names (every resource collapses to `by_id`). The denylist is Tier 1 only | ||
| # (unambiguously generic tokens). Tier 2 natural-key tokens (code|slug|name|type) are intentionally | ||
| # excluded because they are often legitimate (e.g. /countries/{code}); add them only if wanted. | ||
| path-no-generic-id: | ||
| description: Path id parameters must be explicit, not a bare generic identifier. | ||
| given: $.paths[*]~ | ||
| severity: error | ||
| then: | ||
| function: pattern | ||
| functionOptions: | ||
| notMatch: "\\{(id|pk|fk|uid|guid|uuid|oid|key|ref|hash|idx|index|num|no|identifier|value|item|object|entity|record|data|thing)\\}" | ||
|
|
||
| # ENABLED: path PARAMETERS must be snake_case. The kebab-case + lowercase half of our hybrid casing | ||
| # decision (kebab static segments, snake_case params) is ALREADY enforced by the recommended set's | ||
| # `paths-kebab-case` rule, which flags non-kebab STATIC segments and ignores `{param}` segments | ||
| # entirely. This rule adds the parameter half that `paths-kebab-case` does not cover. FastAPI binds a | ||
| # path parameter's name to a Python function argument (which cannot contain hyphens), so snake_case is | ||
| # the only practical casing for parameters. | ||
| path-param-snake-case: | ||
| description: Path parameters must be snake_case. | ||
| given: $.paths[*][*].parameters[?(@.in=='path')].name | ||
| severity: warn | ||
| then: | ||
| function: casing | ||
| functionOptions: | ||
| type: snake | ||
|
|
||
| # ENABLED: query parameters must be camelCase — body params are already camelCase, so matching that | ||
| # for query params keeps frontend clients consistent instead of converting between casings per field. | ||
| query-param-camel-case: | ||
| description: Query parameters must be camelCase. | ||
| given: $.paths[*][*].parameters[?(@.in=='query')].name | ||
| severity: warn | ||
| then: | ||
| function: casing | ||
| functionOptions: | ||
| type: camel | ||
|
|
||
| # ENABLED (gov.au API Response): response bodies must be application/json. `application/problem+json` | ||
| # is also allowed because custom_openapi emits RFC 7807 ProblemDetails error bodies with that media | ||
| # type. Fires zero violations against the current snapshot. | ||
| response-media-type-json: | ||
| description: Response bodies must be application/json (or application/problem+json for errors). | ||
| given: $.paths[*][*].responses[*].content[*]~ | ||
| severity: warn | ||
| then: | ||
| function: pattern | ||
| functionOptions: | ||
| match: "^application/(json|problem\\+json)$" | ||
|
|
||
| # ENABLED (gov.au API Request): request bodies must be application/json. Fires zero violations against | ||
| # the current snapshot. | ||
| request-media-type-json: | ||
| description: Request bodies must be application/json. | ||
| given: $.paths[*][*].requestBody.content[*]~ | ||
| severity: warn | ||
| then: | ||
| function: pattern | ||
| functionOptions: | ||
| match: "^application/json$" | ||
|
|
||
| # ENABLED: POST request bodies must not REQUIRE an id — the backend generates it (a client may still | ||
| # supply an optional one). This targets each entry of the resolved request-body schema's `required` | ||
| # array and asserts none is literally "id"; if `required` is absent, no nodes match and the rule | ||
| # passes. (A schema-of-schema `not/contains` approach was tried first but vacuum reported it as a | ||
| # false positive on every POST body, so this pattern-based form is used instead.) | ||
| create-body-no-required-id: | ||
| description: POST request bodies must not require an id (backend-generated). | ||
| given: $.paths[*].post.requestBody.content.*.schema.required[*] | ||
| resolved: true | ||
| severity: warn | ||
| then: | ||
| function: pattern | ||
| functionOptions: | ||
| notMatch: "^id$" | ||
|
|
||
| # ENABLED (gov.au Naming: nouns not verbs). The recommended set's `no-http-verbs-in-path` already | ||
| # catches HTTP-method names used as path segments; this adds the non-HTTP action verbs it does not | ||
| # cover. Fires zero violations against the current snapshot. | ||
| no-action-verbs-in-path: | ||
| description: Path segments must be nouns, not action verbs. | ||
| given: $.paths[*]~ | ||
| severity: warn | ||
| then: | ||
| function: pattern | ||
| functionOptions: | ||
| notMatch: "/(list|create|add|update|edit|remove|fetch|retrieve|make)(-|/|$)" | ||
|
|
||
| # ENABLED (gov.au API Response): a 201 Created must return the new resource's Location. This fires on | ||
| # POST operations that declare a 201 response, so genuine resource creates (add device, create | ||
| # threshold, create view) declare + emit a Location header. Non-create POST actions should return a | ||
| # non-201 success (e.g. push measurements returns 202 Accepted) so this rule does not apply to them. | ||
| # (A `function: schema` form with a nested `required: [Location]` was tried first but vacuum reported | ||
| # it as a false positive even when the header was present, so this truthy field check is used instead.) | ||
| location-header-on-201: | ||
| description: 201 Created responses must declare a Location header. | ||
| given: $.paths[*].post.responses['201'] | ||
| severity: warn | ||
| then: | ||
| field: headers.Location | ||
| function: truthy | ||
|
|
||
| # ENABLED (gov.au Naming: plural resources). Static path segments should be plural resource nouns. | ||
| # Declarative casing/pattern rules cannot pluralize, so this uses a custom JavaScript function | ||
| # (.config/vacuum-functions/resourcesPlural.js) run via `vacuum lint -f .config/vacuum-functions`. The | ||
| # check is heuristic: it flags a static segment only when it is clearly singular (does not end in "s") | ||
| # and is not in the function's ALLOWLIST of non-collection segments (namespaces, actions, uncountable | ||
| # nouns). Tune that allowlist in the JS file rather than here. | ||
| resources-plural: | ||
| description: Path resource segments should be plural nouns. | ||
| given: $.paths[*]~ | ||
| severity: warn | ||
| then: | ||
| function: resourcesPlural | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toss it into the "Backport" pile next time you're doing copier-base