Class: SpecGuard::RSpec::Schema

Inherits:
Object
  • Object
show all
Defined in:
lib/specguard/rspec/schema.rb

Overview

The OpenTestIntent v1 schema, loaded and applied.

Why loading failure is exit 2 and not exit 1

The exit contract (SPGD-12 ยง1) spends 1 on "an annotation is malformed". If a packaging accident leaves the vendored schema out of the gem, the obvious Ruby implementation lets the exception escape and Ruby exits 1 โ€” so CI reports a malformed annotation and a developer goes hunting for a bad annotation that does not exist. The validator already ruled on this, and this gem follows it. Observed, on a checkout whose schema beside the binary is unreadable:

$ validate-intent x.json
error: could not load schema /repo/schemas/open-test-intent.v1.json: \
    expected a JSON value (line 1, column 1)
$ echo $?
2

Same diagnostic, same 2, whether the run was asking for a verdict or only for --schema-source โ€” the binary states the rule at cmd/validate-intent/main.go, schemaLoadError.

Hence SchemaError: every way loading can fail is caught here and retyped, so the CLI can map the whole class of them to 2 without inspecting exception classes from three different libraries.

The CLI loads this before it scans anything, so a broken schema can never produce a run that reports "0 malformed" having validated nothing.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document:, path: SCHEMA_PATH, renderer: ViolationRenderer.new) ⇒ Schema

Returns a new instance of Schema.



64
65
66
67
68
69
70
# File 'lib/specguard/rspec/schema.rb', line 64

def initialize(document:, path: SCHEMA_PATH, renderer: ViolationRenderer.new)
  @document = document
  @path = path
  @renderer = renderer
  @schemer = JSONSchemer.schema(document)
  reject_unusable_schema!
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



62
63
64
# File 'lib/specguard/rspec/schema.rb', line 62

def document
  @document
end

#pathObject (readonly)

Returns the value of attribute path.



62
63
64
# File 'lib/specguard/rspec/schema.rb', line 62

def path
  @path
end

Class Method Details

.load(path = SCHEMA_PATH) ⇒ Schema

Parameters:

  • path (String) (defaults to: SCHEMA_PATH)

    the vendored schema

Returns:

Raises:

  • (SchemaError)

    if it cannot be read, parsed, or compiled



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/specguard/rspec/schema.rb', line 50

def self.load(path = SCHEMA_PATH)
  document = ::JSON.parse(File.read(path, encoding: "UTF-8"))
  new(document: document, path: path)
rescue StandardError => e
  # Intentionally broad. Reading can raise SystemCallError/IOError,
  # parsing JSON::ParserError, and compiling or meta-validating whatever
  # json_schemer decides an unusable schema document deserves. All of
  # them mean the same thing to the caller, and all of them must be 2
  # rather than an uncaught exception's 1.
  raise SchemaError, "could not load schema #{path}: #{e.message}"
end

Instance Method Details

#violations(intent) ⇒ Array<String>

The validator decides the verdict; the renderer only decides the wording. Deriving "is this annotation valid?" from "could we phrase a sentence about it?" would make an unphrasable violation and a clean annotation the same state โ€” and under Linter that state is a green run, which is this project's signature vacuous green (KB SPGD-78) arriving through the one door the whole rendering strategy exists to close. The gemspec pins ~> 2.5, which admits 2.6 and beyond; the renderer is built from structured fields precisely so a bump degrades to wrong-looking text rather than to silence, and this is what keeps that promise true for error shapes it has never seen.

Parameters:

  • intent (Object)

    one parsed annotation

Returns:

  • (Array<String>)

    reason lines in the validator's grammar and order; empty means the annotation is valid



86
87
88
89
90
91
92
# File 'lib/specguard/rspec/schema.rb', line 86

def violations(intent)
  errors = @schemer.validate(intent).to_a
  return [] if errors.empty?

  rendered = @renderer.render(errors, intent)
  rendered.empty? ? errors.map { |error| unrenderable_reason(error) } : rendered
end