Class: HttpDecoy::HandlerContext

Inherits:
Object
  • Object
show all
Defined in:
lib/http_decoy/handler_context.rb

Overview

The ‘self` inside every route handler block. Provides the full DSL surface: respond, requires_body, validates, body, path_params, query_params, respond_sequence, raise_error.

Defined Under Namespace

Classes: ContractError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rack_request, path_params, call_index: 0) ⇒ HandlerContext

call_index: how many prior requests to this same (method, path) have been logged. Used by respond_sequence to pick the right entry without storing mutable state.



17
18
19
20
21
22
23
24
25
# File 'lib/http_decoy/handler_context.rb', line 17

def initialize(rack_request, path_params, call_index: 0)
  @request     = rack_request
  @path_params = path_params
  @query_params = Rack::Utils.parse_nested_query(rack_request.query_string.to_s)
    .transform_keys(&:to_sym)
  @call_index  = call_index
  @_body       = :unset
  @_response   = nil
end

Instance Attribute Details

#path_paramsObject (readonly)

Returns the value of attribute path_params.



13
14
15
# File 'lib/http_decoy/handler_context.rb', line 13

def path_params
  @path_params
end

#query_paramsObject (readonly)

Returns the value of attribute query_params.



13
14
15
# File 'lib/http_decoy/handler_context.rb', line 13

def query_params
  @query_params
end

#requestObject (readonly)

Returns the value of attribute request.



13
14
15
# File 'lib/http_decoy/handler_context.rb', line 13

def request
  @request
end

Instance Method Details

#bodyObject

Lazily parsed request body — memoized.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/http_decoy/handler_context.rb', line 28

def body
  return @_body unless @_body == :unset

  raw = @request.body&.read || ""
  @request.body&.rewind
  content_type = @request.content_type.to_s

  @_body = if content_type.include?("application/json")
             raw.empty? ? {} : JSON.parse(raw, symbolize_names: true)
           elsif content_type.include?("application/x-www-form-urlencoded")
             Rack::Utils.parse_nested_query(raw).transform_keys(&:to_sym)
           else
             raw
           end
end

#raise_error(type) ⇒ Object

Simulate transport-level failures.



82
83
84
85
86
87
88
89
# File 'lib/http_decoy/handler_context.rb', line 82

def raise_error(type)
  case type
  when :timeout  then raise Timeout::Error, "http_decoy simulated timeout"
  when :reset    then raise Errno::ECONNRESET, "http_decoy simulated connection reset"
  when :refused  then raise Errno::ECONNREFUSED, "http_decoy simulated connection refused"
  else                raise type.is_a?(Class) ? type : RuntimeError, type.to_s
  end
end

#requires_body(*keys) ⇒ Object

Contract assertion: raises ContractError if any key is absent from body.



62
63
64
65
66
67
# File 'lib/http_decoy/handler_context.rb', line 62

def requires_body(*keys)
  keys.each do |key|
    present = body.is_a?(Hash) && (body.key?(key) || body.key?(key.to_s))
    raise ContractError, "#{key} is required in request body" unless present
  end
end

#respond(status, json: nil, text: nil, headers: {}) ⇒ Object

Build and store the response tuple for this request.



45
46
47
48
49
# File 'lib/http_decoy/handler_context.rb', line 45

def respond(status, json: nil, text: nil, headers: {})
  body_str     = json ? JSON.generate(resolve(json)) : text.to_s
  content_type = json ? "application/json" : "text/plain"
  @_response   = [status.to_i, { "Content-Type" => content_type }.merge(headers), [body_str]]
end

#respond_sequence(*responses) ⇒ Object

Stateful sequence: call_index picks which response to use. Each entry is [status, { json: …, text: …, headers: … }]. Wraps around if more calls are made than entries defined.



54
55
56
57
58
59
# File 'lib/http_decoy/handler_context.rb', line 54

def respond_sequence(*responses)
  entry  = responses[@call_index % responses.length]
  status = entry[0]
  opts   = entry[1] || {}
  respond(status, **opts)
end

#responseObject

Internal: the built response tuple, or nil if none was set.



92
93
94
# File 'lib/http_decoy/handler_context.rb', line 92

def response
  @_response
end

#validates(key, type: nil, min: nil, max: nil, inclusion: nil) ⇒ Object

Type / range / enum validation on a body field.

Raises:



70
71
72
73
74
75
76
77
78
79
# File 'lib/http_decoy/handler_context.rb', line 70

def validates(key, type: nil, min: nil, max: nil, inclusion: nil)
  value = body.is_a?(Hash) ? (body[key] || body[key.to_s]) : nil

  raise ContractError, "#{key} must be a #{type}, got #{value.class}" if type && !value.is_a?(type)
  raise ContractError, "#{key} must be >= #{min}, got #{value.inspect}" if min && value < min
  raise ContractError, "#{key} must be <= #{max}, got #{value.inspect}" if max && value > max
  return unless inclusion && !inclusion.include?(value)

  raise ContractError, "#{key} must be one of #{inclusion.inspect}, got #{value.inspect}"
end