Module: Sdp::Drift
- Defined in:
- lib/sdp/drift.rb
Overview
Diffs a newer SDP OpenAPI spec against the vendored pin, restricted to the covered surface (Sdp::Coverage::COVERED_ENDPOINTS). Drives the ‘rake “sdp:drift”` task.
A diff is a REPORT, not a build failure: SDP moving must never break this gem’s own CI, so #run always returns normally (the rake task exits 0). The one hard rule: an unreadable spec prints “drift check unavailable” and never a false “no drift” claim.
Class Method Summary collapse
-
.diff(pinned, newer) ⇒ Object
Compares the two parsed specs over the covered endpoints only.
- .properties(node) ⇒ Object
- .read_spec(path) ⇒ Object
-
.request_findings(label, endpoint, pinned, newer) ⇒ Object
Reports request-body params that became required in the newer spec — existing gem calls would start failing with 400s.
- .request_required(spec, endpoint) ⇒ Object
- .required(node) ⇒ Object
-
.response_findings(label, endpoint, pinned, newer) ⇒ Object
For every schema node the gem reads, reports fields that disappeared (removed/renamed) and fields that became required (shape guarantees changed — e.g. the v0.28→v0.29 usdValue change on balances).
-
.run(pinned_path:, newer_path:, out: $stdout) ⇒ Object
Prints a drift report to ‘out`.
Class Method Details
.diff(pinned, newer) ⇒ Object
Compares the two parsed specs over the covered endpoints only. Returns human-readable finding strings; uncovered endpoints (ramps, issuance, …) never appear here no matter how much they change.
46 47 48 49 50 51 52 53 54 55 |
# File 'lib/sdp/drift.rb', line 46 def diff(pinned, newer) Coverage::COVERED_ENDPOINTS.flat_map do |endpoint| label = "#{endpoint.method.upcase} #{endpoint.path}" newer_operation = newer.dig("paths", endpoint.path, endpoint.method) next [ "#{label}: endpoint missing from newer spec" ] unless newer_operation response_findings(label, endpoint, pinned, newer) + request_findings(label, endpoint, pinned, newer) end end |
.properties(node) ⇒ Object
109 110 111 112 |
# File 'lib/sdp/drift.rb', line 109 def properties(node) props = node.is_a?(Hash) ? node["properties"] : nil props.is_a?(Hash) ? props.keys : [] end |
.read_spec(path) ⇒ Object
57 58 59 60 61 |
# File 'lib/sdp/drift.rb', line 57 def read_spec(path) JSON.parse(File.read(path.to_s)) rescue StandardError nil end |
.request_findings(label, endpoint, pinned, newer) ⇒ Object
Reports request-body params that became required in the newer spec —existing gem calls would start failing with 400s.
93 94 95 96 97 98 99 100 101 |
# File 'lib/sdp/drift.rb', line 93 def request_findings(label, endpoint, pinned, newer) pinned_required = request_required(pinned, endpoint) newer_required = request_required(newer, endpoint) added = newer_required - pinned_required return [] if added.empty? [ "#{label}: request body added required param(s): #{added.join(', ')}" ] end |
.request_required(spec, endpoint) ⇒ Object
103 104 105 106 107 |
# File 'lib/sdp/drift.rb', line 103 def request_required(spec, endpoint) schema = spec.dig("paths", endpoint.path, endpoint.method, "requestBody", "content", "application/json", "schema") required(Coverage.resolve(spec, schema)) end |
.required(node) ⇒ Object
114 115 116 117 |
# File 'lib/sdp/drift.rb', line 114 def required(node) req = node.is_a?(Hash) ? node["required"] : nil req.is_a?(Array) ? req : [] end |
.response_findings(label, endpoint, pinned, newer) ⇒ Object
For every schema node the gem reads, reports fields that disappeared (removed/renamed) and fields that became required (shape guarantees changed — e.g. the v0.28→v0.29 usdValue change on balances).
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/sdp/drift.rb', line 66 def response_findings(label, endpoint, pinned, newer) pinned_schema = Coverage.success_schema(pinned, endpoint) newer_schema = Coverage.success_schema(newer, endpoint) return [ "#{label}: success response #{endpoint.success_status} missing from newer spec" ] unless newer_schema return [] unless pinned_schema # nothing pinned to compare against endpoint.reads.flat_map do |segments, _fields| at = segments.empty? ? "response root" : "response #{segments.join('.')}" pinned_node = Coverage.walk(pinned, pinned_schema, segments) newer_node = Coverage.walk(newer, newer_schema, segments) next [] unless pinned_node next [ "#{label}: #{at} no longer present in newer spec" ] unless newer_node findings = [] removed = properties(pinned_node) - properties(newer_node) findings << "#{label}: #{at} removed/renamed field(s): #{removed.join(', ')}" unless removed.empty? newly_required = required(newer_node) - required(pinned_node) unless newly_required.empty? findings << "#{label}: #{at} field(s) became required: #{newly_required.join(', ')}" end findings end end |
.run(pinned_path:, newer_path:, out: $stdout) ⇒ Object
Prints a drift report to ‘out`. Returns the findings array, or nil when the check could not run at all.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/sdp/drift.rb', line 20 def run(pinned_path:, newer_path:, out: $stdout) newer = read_spec(newer_path) if newer.nil? out.puts "drift check unavailable: could not read newer spec at #{newer_path.inspect}" return nil end pinned = read_spec(pinned_path) if pinned.nil? out.puts "drift check unavailable: could not read pinned spec at #{pinned_path.inspect}" return nil end findings = diff(pinned, newer) if findings.empty? out.puts "No drift on the covered surface (#{Coverage::COVERED_ENDPOINTS.size} endpoints)." else out.puts "Drift detected on the covered surface (#{findings.size} finding(s) — report, not a failure):" findings.each { |finding| out.puts " - #{finding}" } end findings end |