specguard-ruby 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +49 -0
- data/.github/workflows/release.yml +100 -0
- data/LICENSE +21 -0
- data/README.md +1036 -0
- data/Rakefile +28 -0
- data/assets/built-with-yatfa.png +0 -0
- data/bin/specguard-ingest +40 -0
- data/bin/specguard-lint +34 -0
- data/lib/minitest/specguard_plugin.rb +25 -0
- data/lib/specguard/minitest/reporter.rb +244 -0
- data/lib/specguard/rspec/annotation_lookup.rb +363 -0
- data/lib/specguard/rspec/annotation_scanner.rb +190 -0
- data/lib/specguard/rspec/cli.rb +375 -0
- data/lib/specguard/rspec/configuration.rb +472 -0
- data/lib/specguard/rspec/file_selector.rb +276 -0
- data/lib/specguard/rspec/finding.rb +74 -0
- data/lib/specguard/rspec/formatter.rb +1110 -0
- data/lib/specguard/rspec/ingest_cli.rb +942 -0
- data/lib/specguard/rspec/ingest_reporter.rb +280 -0
- data/lib/specguard/rspec/json_reporter.rb +169 -0
- data/lib/specguard/rspec/linter.rb +156 -0
- data/lib/specguard/rspec/payload_normalizer.rb +143 -0
- data/lib/specguard/rspec/scanner.rb +235 -0
- data/lib/specguard/rspec/schemas/open-test-intent.v1.json +15 -0
- data/lib/specguard/rspec/transport.rb +449 -0
- data/lib/specguard/rspec/validator_backend.rb +1450 -0
- data/lib/specguard/rspec/version.rb +14 -0
- data/lib/specguard/rspec.rb +71 -0
- data/lib/specguard/version.rb +11 -0
- data/script/bump-version.sh +128 -0
- metadata +96 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SpecGuard
|
|
6
|
+
module RSpec
|
|
7
|
+
# Relaxes PROTOCOL.md §1's permissive annotation syntax into strict JSON.
|
|
8
|
+
#
|
|
9
|
+
# §1 promises "the linter normalizes before validating" and lists three
|
|
10
|
+
# equivalent forms. This converts all of them:
|
|
11
|
+
#
|
|
12
|
+
# * unquoted keys `{entity:"Order"}` -> `{"entity":"Order"}`
|
|
13
|
+
# * single-quoted strings `{'entity':'Order'}` -> `{"entity":"Order"}`
|
|
14
|
+
# * arbitrary whitespace (`JSON.parse` already tolerates it)
|
|
15
|
+
#
|
|
16
|
+
# plus a trailing comma before `}`/`]` as a courtesy.
|
|
17
|
+
#
|
|
18
|
+
# == Why this is a character scanner, not a `gsub`
|
|
19
|
+
#
|
|
20
|
+
# It is a character-scanner rather than a regex substitution **so quoted
|
|
21
|
+
# content is never rewritten** — a `behavior` sentence containing `it's` or
|
|
22
|
+
# `{` is passed through untouched. Any `gsub`-then-`JSON.parse` port
|
|
23
|
+
# corrupts exactly those values; the examples under "quoted content is
|
|
24
|
+
# never rewritten" in `payload_normalizer_spec.rb` pin them.
|
|
25
|
+
#
|
|
26
|
+
# The payload is *never* `eval`'d. It is attacker-controllable text taken
|
|
27
|
+
# from a comment in someone's spec file, so it only ever reaches
|
|
28
|
+
# `JSON.parse`, which cannot execute anything.
|
|
29
|
+
#
|
|
30
|
+
# == The bare-word rule
|
|
31
|
+
#
|
|
32
|
+
# A bare word is quoted **only in key position**. A bare word used as a
|
|
33
|
+
# value (`{layer: request}`) is left alone so it still fails downstream —
|
|
34
|
+
# the protocol relaxes keys and quote style, never value quoting.
|
|
35
|
+
#
|
|
36
|
+
# Ported from open-test-intent's `bin/validate-intent`
|
|
37
|
+
# (`normalize_payload` / `_requote`).
|
|
38
|
+
module PayloadNormalizer
|
|
39
|
+
# Anchored at the scan position (`\G`), so it matches AT i rather than
|
|
40
|
+
# searching from it. Explicit ASCII ranges, per PROTOCOL.md §1 — a Unicode
|
|
41
|
+
# word class would make the accepted surface syntax depend on the regex
|
|
42
|
+
# engine, which is what one specification exists to prevent.
|
|
43
|
+
BARE_WORD = /\G[A-Za-z_$][A-Za-z0-9_$]*/.freeze
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# @param raw [String] the object literal as written in the source
|
|
48
|
+
# @return [String] strict JSON, ready for `JSON.parse`
|
|
49
|
+
# @raise [ScanError] on an unterminated string literal
|
|
50
|
+
def normalize(raw)
|
|
51
|
+
out = +""
|
|
52
|
+
i = 0
|
|
53
|
+
length = raw.length
|
|
54
|
+
|
|
55
|
+
while i < length
|
|
56
|
+
char = raw[i]
|
|
57
|
+
|
|
58
|
+
# Already-strict double-quoted string: copy verbatim, contents and all.
|
|
59
|
+
if char == '"'
|
|
60
|
+
finish = AnnotationScanner.scan_string(raw, i, '"')
|
|
61
|
+
out << raw[i...finish]
|
|
62
|
+
i = finish
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Single-quoted string: re-emit its body as a JSON string.
|
|
67
|
+
if char == "'"
|
|
68
|
+
finish = AnnotationScanner.scan_string(raw, i, "'")
|
|
69
|
+
out << requote(raw[(i + 1)...(finish - 1)])
|
|
70
|
+
i = finish
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if (match = BARE_WORD.match(raw, i))
|
|
75
|
+
word = match[0]
|
|
76
|
+
after = match.end(0)
|
|
77
|
+
out << (key_position?(raw, after) ? JSON.generate(word) : word)
|
|
78
|
+
i = after
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Trailing comma before a closing bracket: drop it.
|
|
83
|
+
if char == "," && closes_after?(raw, i + 1)
|
|
84
|
+
i += 1
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
out << char
|
|
89
|
+
i += 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
out
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Re-emits the body of a single-quoted string as a double-quoted JSON
|
|
96
|
+
# string, preserving every escape that JSON understands.
|
|
97
|
+
#
|
|
98
|
+
# @param body [String] the string's contents, without its surrounding quotes
|
|
99
|
+
# @return [String] a complete JSON string literal, quotes included
|
|
100
|
+
def requote(body)
|
|
101
|
+
out = +""
|
|
102
|
+
i = 0
|
|
103
|
+
length = body.length
|
|
104
|
+
|
|
105
|
+
while i < length
|
|
106
|
+
char = body[i]
|
|
107
|
+
|
|
108
|
+
if char == "\\"
|
|
109
|
+
nxt = i + 1 < length ? body[i + 1] : ""
|
|
110
|
+
# `\'` is meaningless in JSON — unescape it; keep every other escape.
|
|
111
|
+
out << (nxt == "'" ? "'" : char + nxt)
|
|
112
|
+
i += 2
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
out << (char == '"' ? '\"' : char)
|
|
117
|
+
i += 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
%("#{out}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# True when the next non-space character after `pos` is a `:`, i.e. the
|
|
124
|
+
# bare word just scanned is a key rather than a value.
|
|
125
|
+
def key_position?(raw, pos)
|
|
126
|
+
probe = skip_space(raw, pos)
|
|
127
|
+
probe < raw.length && raw[probe] == ":"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# True when the next non-space character at or after `pos` closes a
|
|
131
|
+
# bracket, i.e. the comma just scanned was a trailing comma.
|
|
132
|
+
def closes_after?(raw, pos)
|
|
133
|
+
probe = skip_space(raw, pos)
|
|
134
|
+
probe < raw.length && (raw[probe] == "}" || raw[probe] == "]")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def skip_space(raw, pos)
|
|
138
|
+
pos += 1 while pos < raw.length && raw[pos].match?(/\s/)
|
|
139
|
+
pos
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SpecGuard
|
|
6
|
+
module RSpec
|
|
7
|
+
# Runs the discovery pipeline over source files and returns {Finding}s.
|
|
8
|
+
#
|
|
9
|
+
# Pipeline, per `@intent:` token:
|
|
10
|
+
#
|
|
11
|
+
# {AnnotationScanner} -> {PayloadNormalizer} -> `JSON.parse`
|
|
12
|
+
#
|
|
13
|
+
# This is the whole of the *input* half of the linter. It deliberately stops
|
|
14
|
+
# at "an annotation is a Hash": nothing here loads or applies the
|
|
15
|
+
# OpenTestIntent schema, so a Finding with an `intent` is merely
|
|
16
|
+
# syntactically sound, not valid.
|
|
17
|
+
module Scanner
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @param paths [Enumerable<String>]
|
|
21
|
+
# @return [Array<Finding>] in file-then-line order
|
|
22
|
+
def scan_files(paths)
|
|
23
|
+
paths.flat_map { |path| scan_file(path) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param path [String]
|
|
27
|
+
# @return [Array<Finding>] one per `@intent:` token; empty when the file
|
|
28
|
+
# carries none. A file that cannot be read yields a single Finding at
|
|
29
|
+
# line 0 rather than raising — one bad file must not abort the run.
|
|
30
|
+
#
|
|
31
|
+
# RATIFIED DIFFERENCE from the binary, for a path that does not exist.
|
|
32
|
+
# `validate-intent` expands its arguments as glob PATTERNS, so a name
|
|
33
|
+
# matching nothing is a statement about the pattern —
|
|
34
|
+
# `error: no file(s) match '<path>'`, on stderr, before any file is
|
|
35
|
+
# opened. This linter does no globbing: explicit files are checked as
|
|
36
|
+
# given and `--changed` derives its list from git, so every argument
|
|
37
|
+
# here is a PATH, and an unopenable one is a read failure OF THAT PATH —
|
|
38
|
+
# reported on stdout with the errno the binary never had occasion to
|
|
39
|
+
# name. Adopting the binary's wording would mean giving this gem a
|
|
40
|
+
# globber first, and changing what `specguard-lint 'foo*_spec.rb'` means
|
|
41
|
+
# for everyone already using it.
|
|
42
|
+
#
|
|
43
|
+
# Both tools exit 1 and both name the file; that much is asserted in
|
|
44
|
+
# spec/specguard/rspec/validator_backend_spec.rb, along with the case
|
|
45
|
+
# that matters more — a missing file does not stop either tool checking
|
|
46
|
+
# the good files named beside it.
|
|
47
|
+
#
|
|
48
|
+
# A path that exists and is NOT A REGULAR FILE lands in the same rescue
|
|
49
|
+
# and is the same ratified difference one step further out: this rescue
|
|
50
|
+
# has an errno and reports it (`Is a directory @ io_fread - <path>`),
|
|
51
|
+
# while the binary's glob filters non-regular matches away and answers
|
|
52
|
+
# exactly as it does for a name matching nothing. Ruby tells the two
|
|
53
|
+
# apart and the backend cannot — asserted under "a path that is not a
|
|
54
|
+
# regular file" in that same spec.
|
|
55
|
+
def scan_file(path)
|
|
56
|
+
begin
|
|
57
|
+
text = File.read(path, encoding: "UTF-8")
|
|
58
|
+
rescue SystemCallError, IOError => e
|
|
59
|
+
return [Finding.new(file: path, line: 0, problem: "could not read file: #{e.message}",
|
|
60
|
+
kind: Finding::KIND_READ)]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
scan_text(text, file: path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param text [String] source of one file
|
|
67
|
+
# @param file [String] the path to record on each Finding
|
|
68
|
+
# @return [Array<Finding>]
|
|
69
|
+
def scan_text(text, file:)
|
|
70
|
+
# An invalid byte sequence would make every String operation below raise
|
|
71
|
+
# from deep inside the scanner. Report it as this file's one problem.
|
|
72
|
+
#
|
|
73
|
+
# RATIFIED DIFFERENCE from the binary, in the REASON TEXT only. Both
|
|
74
|
+
# refuse the file — PROTOCOL.md §1.1 makes UTF-8 part of what a JSON
|
|
75
|
+
# text IS, and neither side repairs the bytes — and both name the
|
|
76
|
+
# CONDITION rather than an offset: the binary says `input is not
|
|
77
|
+
# well-formed UTF-8 (PROTOCOL.md §1.1 requires it)`, this says
|
|
78
|
+
# `invalid UTF-8 byte sequence`. Neither wording is specified, so
|
|
79
|
+
# neither is wrong; what matters is that both classify it as a READ
|
|
80
|
+
# failure and neither substitutes U+FFFD and carries on.
|
|
81
|
+
#
|
|
82
|
+
# What is shared is asserted in spec/specguard/rspec/validator_backend_spec.rb,
|
|
83
|
+
# under "the not-well-formed-UTF-8 text is each backend's own": same
|
|
84
|
+
# classification, same file, same `FAIL <file> — could not read file: `
|
|
85
|
+
# prefix, a non-empty reason on both sides, and exit 1 — and the tails
|
|
86
|
+
# are asserted to still DIFFER, so converging them fails that file
|
|
87
|
+
# rather than leaving this comment stale.
|
|
88
|
+
unless text.valid_encoding?
|
|
89
|
+
return [Finding.new(file: file, line: 0, problem: "could not read file: invalid UTF-8 byte sequence",
|
|
90
|
+
kind: Finding::KIND_READ)]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
AnnotationScanner.each_intent(text).map do |line_no, raw, problem|
|
|
94
|
+
if problem
|
|
95
|
+
Finding.new(file: file, line: line_no, problem: problem, kind: Finding::KIND_EXTRACTION)
|
|
96
|
+
else
|
|
97
|
+
parse(raw, file: file, line: line_no)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# RATIFIED DIFFERENCE from the binary. Two of them, and only the first is
|
|
103
|
+
# a difference in reason TEXT.
|
|
104
|
+
#
|
|
105
|
+
# == (1) The wording, when both parsers reject the payload
|
|
106
|
+
#
|
|
107
|
+
# `PayloadNormalizer` rescues PROTOCOL.md §1's permissive syntax on both
|
|
108
|
+
# sides, so a payload it can fix renders identically. What reaches
|
|
109
|
+
# `JSON.parse` still broken — a bare-word VALUE, a key that is not a key
|
|
110
|
+
# — is described by whichever JSON parser is doing the parsing: this
|
|
111
|
+
# interpolates Ruby's `JSON::ParserError#message`, while the binary uses
|
|
112
|
+
# its own. So `{bad_key}` is `expected object key, got 'bad_key}' at line
|
|
113
|
+
# 1 column 2` here and `expected a double-quoted property name (line 1,
|
|
114
|
+
# column 2)` there.
|
|
115
|
+
#
|
|
116
|
+
# Reproducing that tail would mean carrying a second parser's diagnostics
|
|
117
|
+
# in a gem whose entire reason for vendoring the schema is to owe
|
|
118
|
+
# open-test-intent nothing at runtime — the argument `scan_text` makes
|
|
119
|
+
# about the DECODER, applied verbatim to the PARSER. PROTOCOL.md
|
|
120
|
+
# specifies the accepted LANGUAGE, not the prose a validator refuses in,
|
|
121
|
+
# so two spellings of one refusal are both conformant.
|
|
122
|
+
#
|
|
123
|
+
# What is shared is asserted in
|
|
124
|
+
# spec/specguard/rspec/validator_backend_spec.rb: same classification,
|
|
125
|
+
# same file, same LINE (unlike a read failure, this one is line-scoped and
|
|
126
|
+
# both agree on the line and even the column), same
|
|
127
|
+
# `could not parse annotation: ` prefix, same counts, and exit 1. Only the
|
|
128
|
+
# tail is unpinned.
|
|
129
|
+
#
|
|
130
|
+
# == (2) THE ACCEPTANCE SET: where the two parsers take different input
|
|
131
|
+
#
|
|
132
|
+
# (1) is about how the two spell the same refusal. This is about payloads
|
|
133
|
+
# where they do not both refuse — a bigger difference, and the generator
|
|
134
|
+
# (1) was hiding.
|
|
135
|
+
#
|
|
136
|
+
# This USED to be a three-member set in which the binary was the
|
|
137
|
+
# permissive side, because its parser reproduced a foreign runtime's
|
|
138
|
+
# grammar that `PROTOCOL.md` had never specified. PROTOCOL.md §1.1 states
|
|
139
|
+
# the grammar now: an RFC 8259 JSON text, with the three points that RFC
|
|
140
|
+
# leaves open settled explicitly. The binary refuses the non-finite
|
|
141
|
+
# literals (§1.1(b)), unpaired surrogate escapes (§1.1(a)) and nesting
|
|
142
|
+
# past 100 (§1.1(c)), and `JSON.parse` refuses or limits all three too, so
|
|
143
|
+
# those three have CONVERGED.
|
|
144
|
+
#
|
|
145
|
+
# WHAT SURVIVES RUNS THE OTHER WAY: this parser is now the permissive one.
|
|
146
|
+
#
|
|
147
|
+
# Read that as scoped to PARSING, which is all this register has ever
|
|
148
|
+
# covered. It is not a statement that the two backends agree everywhere
|
|
149
|
+
# else: the stage BEFORE this one diverged too, and in the opposite
|
|
150
|
+
# direction. Until SPGD-512 the binary's `@intent:` payload search was
|
|
151
|
+
# unbounded where {AnnotationScanner#payload_brace} bounds it at the next
|
|
152
|
+
# token, so a malformed token followed by a well-formed one was captured
|
|
153
|
+
# as valid by the binary and reported no-payload by this gem — binary
|
|
154
|
+
# permissive, gem strict. The Go port of that bound closed it. The lesson
|
|
155
|
+
# worth keeping is that a divergence can live at extraction as easily as
|
|
156
|
+
# at parse, so "what survives" below is the parser's list, not the
|
|
157
|
+
# backends' list.
|
|
158
|
+
#
|
|
159
|
+
# * A LONE LOW surrogate escape (`\udc00`-`\udfff`). `JSON.parse`
|
|
160
|
+
# accepts it; §1.1(a) refuses it, because a surrogate escape must form
|
|
161
|
+
# a pair. Ruby refuses only the HIGH half, which is why the rule here
|
|
162
|
+
# is narrower than "surrogate escapes diverge".
|
|
163
|
+
# * The nesting BOUNDARY, by exactly one level and only for an EMPTY
|
|
164
|
+
# container. §1.1(c) refuses any container deeper than 100. Ruby
|
|
165
|
+
# checks the depth when it is about to parse a VALUE, so a container
|
|
166
|
+
# sitting at depth 101 with nothing in it is never checked and is
|
|
167
|
+
# accepted; put anything inside it and Ruby refuses too. Measured on
|
|
168
|
+
# json 2.21.2 and pinned, both halves, in
|
|
169
|
+
# `spec/specguard/rspec/validator_backend_spec.rb`.
|
|
170
|
+
#
|
|
171
|
+
# The surrogate one is not merely a verdict difference. What `JSON.parse`
|
|
172
|
+
# returns for `"\udc00"` is a String whose `valid_encoding?` is FALSE: it
|
|
173
|
+
# cannot be re-serialised and cannot cross the ingest transport, so a
|
|
174
|
+
# payload this parser calls valid is one the gem cannot send. That is the
|
|
175
|
+
# cost §1.1(a) exists to remove, and it is still paid on this path.
|
|
176
|
+
#
|
|
177
|
+
# It produces two shapes, depending on whether the payload this parser
|
|
178
|
+
# accepts then passes the schema. BOTH ARE REACHABLE, one per survivor:
|
|
179
|
+
#
|
|
180
|
+
# (v) NOT schema-valid — the CLASSIFICATION differs. The NESTING
|
|
181
|
+
# survivor lands here and only here. A container at depth 101 is
|
|
182
|
+
# a container, so it can never occupy a schema-legal slot: every
|
|
183
|
+
# value the schema permits is a string or an array of strings. So
|
|
184
|
+
# a payload deep enough to diverge is a payload the schema refuses,
|
|
185
|
+
# and the two tools refuse it for different stated reasons —
|
|
186
|
+
#
|
|
187
|
+
# binary: "kind": "parse" nesting is deeper than 100 levels
|
|
188
|
+
# (PROTOCOL.md §1.1(c))
|
|
189
|
+
# gem: "kind": "schema" preconditions[0]: expected type
|
|
190
|
+
# string, got array
|
|
191
|
+
#
|
|
192
|
+
# The VERDICT agrees (both fail, both exit 1); only the `kind` and
|
|
193
|
+
# the reason differ.
|
|
194
|
+
# (vi) schema-valid — the VERDICT differs. The SURROGATE survivor lands
|
|
195
|
+
# here: it lives inside a string, and a string is what the schema
|
|
196
|
+
# permits. This path reports nothing and exits 0; the backend
|
|
197
|
+
# reports a parse failure and exits 1.
|
|
198
|
+
#
|
|
199
|
+
# RATIFIED, and the reason is scope rather than preference. This gem's
|
|
200
|
+
# hand-rolled validation logic is slated for REMOVAL by the roadmap that
|
|
201
|
+
# owns the binary, not for repair; and closing the gap here would be a
|
|
202
|
+
# change to the DEFAULT path, which the slice that introduced
|
|
203
|
+
# `ValidatorBackend` explicitly holds fixed. Whoever removes this parser
|
|
204
|
+
# closes it by deletion.
|
|
205
|
+
#
|
|
206
|
+
# Asserted from both sides — what this parser accepts, the convergence,
|
|
207
|
+
# and both surviving divergences — in
|
|
208
|
+
# `spec/specguard/rspec/validator_backend_spec.rb` under "the JSON
|
|
209
|
+
# acceptance set".
|
|
210
|
+
#
|
|
211
|
+
# Note that `JSON::NestingError` is a subclass of `JSON::ParserError`, so
|
|
212
|
+
# a nesting refusal Ruby DOES make is already carried by the rescue below
|
|
213
|
+
# and needs no clause of its own. That is about the refusals the two
|
|
214
|
+
# share; it is not a claim that the classification always agrees, which
|
|
215
|
+
# at depth 101 it does not — see (v).
|
|
216
|
+
#
|
|
217
|
+
# @return [Finding]
|
|
218
|
+
def parse(raw, file:, line:)
|
|
219
|
+
intent = JSON.parse(PayloadNormalizer.normalize(raw))
|
|
220
|
+
|
|
221
|
+
# A bare `[...]` or scalar is syntactically fine JSON but is not an
|
|
222
|
+
# annotation. Reject it here so downstream stages can assume a Hash.
|
|
223
|
+
unless intent.is_a?(Hash)
|
|
224
|
+
return Finding.new(file: file, line: line, kind: Finding::KIND_PARSE,
|
|
225
|
+
problem: "annotation payload is not an object")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
Finding.new(file: file, line: line, intent: intent)
|
|
229
|
+
rescue ScanError, JSON::ParserError => e
|
|
230
|
+
Finding.new(file: file, line: line, kind: Finding::KIND_PARSE,
|
|
231
|
+
problem: "could not parse annotation: #{e.message}")
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/yatfa-ai/open-test-intent/schema-v1.0/schemas/open-test-intent.v1.json",
|
|
4
|
+
"title": "OpenTestIntent v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"entity": { "type": "string", "minLength": 2 },
|
|
9
|
+
"action": { "type": "string", "minLength": 2 },
|
|
10
|
+
"behavior": { "type": "string", "minLength": 15 },
|
|
11
|
+
"layer": { "type": "string", "enum": ["unit", "integration", "request", "system"] },
|
|
12
|
+
"preconditions": { "type": "array", "items": { "type": "string" } }
|
|
13
|
+
},
|
|
14
|
+
"required": ["entity", "action", "behavior", "layer"]
|
|
15
|
+
}
|