Module: Restless::Injection

Defined in:
lib/restless/injection.rb

Overview

CONTRACT.md section 10. What the SDK adds to the customer's own responses.

Class Method Summary collapse

Class Method Details

.apply_body(body, content_type, debug) ⇒ Object

INJECT-003. Merge debug into the response body ONLY when the body is a JSON OBJECT (not an array, not a scalar) and the content type says JSON.

Returns the original body untouched on any parse failure: SAFETY-001 outranks everything here.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/restless/injection.rb', line 83

def apply_body(body, content_type, debug)
  return body if body.nil? || body.empty? || debug.nil?
  return body unless Text.full_lower(content_type.to_s).include?("application/json")

  begin
    parsed = JSON.parse(body, max_nesting: false)
  rescue StandardError
    return body
  end
  return body unless parsed.is_a?(Hash)

  JSON.generate(parsed.merge("debug" => debug))
rescue StandardError
  body
end

.build(status:, request_id:, base_url:, prefix: nil, recovery: nil, method: nil, path: nil, docs_url: nil) ⇒ Object

INJECT-001..004, INJECT-006. Returns the headers to set plus the debug object to merge into a JSON body, or nil when nothing should be injected.



36
37
38
39
40
41
42
43
44
45
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
# File 'lib/restless/injection.rb', line 36

def build(status:, request_id:, base_url:, prefix: nil, recovery: nil,
          method: nil, path: nil, docs_url: nil)
  return nil if status < 400 # INJECT-001

  display = RequestId.format_request_id(request_id, prefix)
  # INJECT-006: the server-supplied docsUrl when one has been learned,
  # else the configured base URL. One batch of staleness after a
  # docs-domain change is accepted.
  log_host = docs_url.nil? || docs_url.empty? ? base_url : docs_url
  log_url = "#{log_host}/logs/#{request_id}"
  debug_cmd = "npx api debug #{display}"

  # Per-request "dig-in" URL the calling agent (often an AI) can fetch for
  # concrete next steps. Deliberately LEGIBLE: it ends in `<slug>.md` so it
  # reads as documentation rather than a tracking blob. The first segment
  # is the same public request id already in `debug.log`, so the dashboard
  # can correlate the follow-up without any new tracking token.
  slug = recovery_slug(method, path)
  dig_in = "For the accepted parameters and next steps, " \
           "fetch #{log_host}/p/#{request_id}/#{slug}.md"
  # INJECT-004: a cached recovery message precedes the dig-in line,
  # separated by a blank line.
  recovery_text =
    if recovery.nil? || recovery.empty?
      dig_in
    else
      "#{recovery}\n\n#{dig_in}"
    end

  {
    headers: {
      "x-log-url" => log_url, # INJECT-002
      "x-debug" => debug_cmd
    },
    debug: {
      "log" => log_url,
      "cli" => debug_cmd,
      "recovery" => recovery_text
    }
  }
end

.recovery_slug(method = nil, path = nil) ⇒ Object

INJECT-005. Legible URL slug for the recovery dig-in path, derived from method + route pattern: GET /car/{id} becomes get-car-id.

The server resolves it back to an OpenAPI operation by matching the same scheme, so this MUST stay in sync with the app's recoverySlug (INJECT-007).



19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/restless/injection.rb', line 19

def recovery_slug(method = nil, path = nil)
  m = Text.full_lower(method.to_s)
  # JavaScript's `trim` strips the PRIM-002 set; Ruby's `strip` strips a
  # different one in both directions.
  p = Text.ws_trim(path.to_s)
  return "unknown" if m.empty? || p.empty?

  flat = p.gsub(%r{[/{}:]+}, "-")
          .gsub(/[^a-zA-Z0-9\-]/, "")
          .gsub(/-+/, "-")
  # `\A` / `\z`, not `^` / `$`: Ruby's are line anchors (PRIM-005).
  flat = flat.sub(/\A-/, "").sub(/-\z/, "")
  flat.empty? ? m : "#{m}-#{flat}"
end