Module: SpecGuard::RSpec::PayloadNormalizer
- Defined in:
- lib/specguard/rspec/payload_normalizer.rb
Overview
Relaxes PROTOCOL.md §1's permissive annotation syntax into strict JSON.
§1 promises "the linter normalizes before validating" and lists three equivalent forms. This converts all of them:
* unquoted keys `{entity:"Order"}` -> `{"entity":"Order"}`
* single-quoted strings `{'entity':'Order'}` -> `{"entity":"Order"}`
* arbitrary whitespace (`JSON.parse` already tolerates it)
plus a trailing comma before }/] as a courtesy.
Why this is a character scanner, not a gsub
It is a character-scanner rather than a regex substitution so quoted
content is never rewritten — a behavior sentence containing it's or
{ is passed through untouched. Any gsub-then-JSON.parse port
corrupts exactly those values; the fixture suite pins all four cases.
The payload is never eval'd. It is attacker-controllable text taken
from a comment in someone's spec file, so it only ever reaches
JSON.parse, which cannot execute anything.
The bare-word rule
A bare word is quoted only in key position. A bare word used as a
value ({layer: request}) is left alone so it still fails downstream —
the protocol relaxes keys and quote style, never value quoting.
Ported from open-test-intent's bin/validate-intent
(normalize_payload / _requote).
Constant Summary collapse
- BARE_WORD =
Anchored at the scan position (
\G), mirroring Python'sre.match(s, i). /\G[A-Za-z_$][A-Za-z0-9_$]*/.freeze
Class Method Summary collapse
-
.closes_after?(raw, pos) ⇒ Boolean
True when the next non-space character at or after
poscloses a bracket, i.e. -
.key_position?(raw, pos) ⇒ Boolean
True when the next non-space character after
posis a:, i.e. -
.normalize(raw) ⇒ String
Strict JSON, ready for
JSON.parse. -
.requote(body) ⇒ String
Re-emits the body of a single-quoted string as a double-quoted JSON string, preserving every escape that JSON understands.
- .skip_space(raw, pos) ⇒ Object
Class Method Details
.closes_after?(raw, pos) ⇒ Boolean
True when the next non-space character at or after pos closes a
bracket, i.e. the comma just scanned was a trailing comma.
128 129 130 131 |
# File 'lib/specguard/rspec/payload_normalizer.rb', line 128 def closes_after?(raw, pos) probe = skip_space(raw, pos) probe < raw.length && (raw[probe] == "}" || raw[probe] == "]") end |
.key_position?(raw, pos) ⇒ Boolean
True when the next non-space character after pos is a :, i.e. the
bare word just scanned is a key rather than a value.
121 122 123 124 |
# File 'lib/specguard/rspec/payload_normalizer.rb', line 121 def key_position?(raw, pos) probe = skip_space(raw, pos) probe < raw.length && raw[probe] == ":" end |
.normalize(raw) ⇒ String
Returns strict JSON, ready for JSON.parse.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/specguard/rspec/payload_normalizer.rb', line 46 def normalize(raw) out = +"" i = 0 length = raw.length while i < length char = raw[i] # Already-strict double-quoted string: copy verbatim, contents and all. if char == '"' finish = AnnotationScanner.scan_string(raw, i, '"') out << raw[i...finish] i = finish next end # Single-quoted string: re-emit its body as a JSON string. if char == "'" finish = AnnotationScanner.scan_string(raw, i, "'") out << requote(raw[(i + 1)...(finish - 1)]) i = finish next end if (match = BARE_WORD.match(raw, i)) word = match[0] after = match.end(0) out << (key_position?(raw, after) ? JSON.generate(word) : word) i = after next end # Trailing comma before a closing bracket: drop it. if char == "," && closes_after?(raw, i + 1) i += 1 next end out << char i += 1 end out end |
.requote(body) ⇒ String
Re-emits the body of a single-quoted string as a double-quoted JSON string, preserving every escape that JSON understands.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/specguard/rspec/payload_normalizer.rb', line 96 def requote(body) out = +"" i = 0 length = body.length while i < length char = body[i] if char == "\\" nxt = i + 1 < length ? body[i + 1] : "" # `\'` is meaningless in JSON — unescape it; keep every other escape. out << (nxt == "'" ? "'" : char + nxt) i += 2 next end out << (char == '"' ? '\"' : char) i += 1 end %("#{out}") end |
.skip_space(raw, pos) ⇒ Object
133 134 135 136 |
# File 'lib/specguard/rspec/payload_normalizer.rb', line 133 def skip_space(raw, pos) pos += 1 while pos < raw.length && raw[pos].match?(/\s/) pos end |