Module: SpecGuard::RSpec::JSONReporter
- Defined in:
- lib/specguard/rspec/json_reporter.rb
Overview
specguard-lint --json: the machine-readable renderer over the same
Array<Linter::Result> the text report is built from.
Why this is a renderer and not a feature
CLI#check has exactly one branch on which validator produced the
verdicts, and it closes immediately: both arms return the same
Linter::Result list. Everything downstream of it — the FAIL blocks, the
summary line, the exit code — is shared code rather than two renderers
that have to be kept in step. This hangs off that same point, so it
inherits that guarantee: the --json document and the text report are the
same checks rendered twice, not a second implementation that can drift.
What the text renderer destroys at the print site is the structure. A
Linter::Result carries file, line, kind, problem and reasons; the FAIL
block flattens all five into prose, leaving a consumer the exit code and a
regex. kind is the field that suffers most — Finding's own comment
says it is carried "because a failed extraction and an unparseable payload
both land in problem, which makes the two indistinguishable downstream
once flattened to prose", and then the CLI flattens it to prose.
The shape, and why it is the port's shape
This mirrors validate-intent --json --source
(open-test-intent, cmd/validate-intent/report.go) key for key:
{"schema", "mode", "ok", "summary": {"files", "annotations", "failed"},
"findings": [{"file", "line", "ok", "kind", "errors"}, ...]}
The gem is already a consumer of exactly this document —
ValidatorBackend::Runner runs --source --json and reconstructs
Linter::Results from it — so emitting a different shape would mean a
consumer of both tools needs two parsers for one protocol. It does not
try to be byte-identical to the port (Ruby's generator does not escape
non-ASCII where Python's json.dumps does), only key-, type- and
value-identical, which is what a parser sees.
Three properties are load-bearing for a consumer and are asserted rather
than described (spec/specguard/rspec/cli_spec.rb):
* `errors` is ALWAYS a list of strings — `reasons` when the schema
rejected the annotation, `[problem]` when discovery could not produce
one at all, `[]` when it passed. Never null, never a bare string:
`report.go:23-26` is explicit that a consumer must never branch on its
type, and this is the one place the gem's mutually-exclusive
`problem`/`reasons` pair is normalised into the port's single list.
* `line` is null exactly where the finding is not line-scoped, which is
{Linter::Result#line_scoped?} — the same rule `#location` uses to
print `file` rather than `file:0`. `:0` is not somewhere a reader can
go, and emitting it here would hand every CI annotation and quickfix
consumer a line that does not exist.
* `kind` is null on a passing finding and one of `extraction`, `parse`,
`read`, `schema` otherwise. The port has a fifth, `no-match`, which
cannot appear here: {ValidatorBackend} maps it onto
{Finding::KIND_READ} at the seam, because on this side of it a path
that matched nothing *is* a named path that could not be opened.
The counts are handed in, not recomputed
summary.files and summary.annotations are passed by CLI, which
computes them once for both renderers. Recounting them here would let the
text summary line and this document disagree about the same run, which is
the failure mode the shared-downstream design exists to prevent — and a
disagreement between two renderers of one result list is unfalsifiable
from the outside.
ok is likewise handed in, derived from the exit code the text path would
also have produced rather than recomputed from the findings, following
report.go:84-89 for the same reason.
summary.failed, by contrast, is counted here — from the findings this
document actually emitted, so failed always equals the number of entries
with "ok": false. Note that it is NOT the text summary's "M malformed":
that clause counts malformed annotations and reports unreadable files in
a separate clause, while failed counts every failing finding, read
failures included, exactly as the port's Emit does. Two different
questions, each answered the same way in both implementations.
What is deliberately NOT in the document
The backend provenance line (SPGD-247 — one stderr line per run naming the
implementation that produced the verdicts) is not duplicated in here.
Two reasons, and the decision is recorded rather than defaulted into:
it would be the first key by which this document differs from the port's,
reintroducing the second parser this shape exists to avoid; and provenance
would then have two homes that can disagree about one fact, which is the
shape SPGD-247 was written to close, not to widen. 2> the stderr stream
and read the line; it is still exactly one line, on every run, on both
arms.
Constant Summary collapse
- SCHEMA_ID =
The port's
jsonSchemaID(report.go:19), and the basename of the gem's own vendored schema. Pinned to each other by a spec: the document names the protocol it validated against, so it must not be able to name one the gem does not carry. "open-test-intent.v1.json"- MODE =
The port's
--sourcemode: annotations found in spec sources, which is the only thingspecguard-lintdoes. It is not the selection mode (--changedvs explicit files) — that is this tool's own vocabulary and the port has no field for it. Emitting"changed"here would tell a consumer that already readsvalidate-intent --jsonthat it is looking at a mode that does not exist. "source"
Class Method Summary collapse
-
.render(results, files:, annotations:, ok:) ⇒ String
One JSON document, without a trailing newline.
Class Method Details
.render(results, files:, annotations:, ok:) ⇒ String
Returns one JSON document, without a trailing newline.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/specguard/rspec/json_reporter.rb', line 118 def self.render(results, files:, annotations:, ok:) findings = results.map { |result| finding(result) } JSON.pretty_generate( "schema" => SCHEMA_ID, "mode" => MODE, "ok" => ok, "summary" => { "files" => files, "annotations" => annotations, "failed" => findings.count { |entry| !entry["ok"] } }, "findings" => findings ) end |