Class: Rooibos::Command::Http

Inherits:
Object
  • Object
show all
Includes:
Custom
Defined in:
lib/rooibos/command/http.rb

Overview

Performs HTTP requests and sends the response as a message.

Applications fetch data from APIs. Users expect responsive interfaces while requests complete. Managing HTTP connections, timeouts, and threading manually is error-prone.

This command executes HTTP requests off the main thread. The runtime dispatches it and routes the response back to your update function as a Message::HttpResponse.

Use it to fetch API data, post forms, or interact with web services.

Prefer the Command.http factory method for convenience. The constructor supports flexible DWIM (Do What I Mean) arity.

Example

# Using the factory method (recommended)
Command.http(:get, "/api/users", :users)
Command.http(get: "/api/users", envelope: :users)
Command.http(:post, "/api/users", '{"name":"Jo"}', :created)

# Using the class directly
Http.new(:get, "/api/users", :users)

# Pattern-match on the response
def update(message, model)
  case message
  in { type: :http, envelope: :users, status: 200, body: }
    model.with(users: JSON.parse(body))
  in { type: :http, envelope: :users, error: }
    model.with(error:)
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Custom

#deconstruct_keys, #rooibos_command?

Class Method Details

.new(*args, method: nil, url: nil, envelope: nil, headers: nil, body: nil, timeout: nil, parser: nil, get: nil, post: nil, put: nil, patch: nil, delete: nil) ⇒ Object

Creates an HTTP request command.

Supports flexible DWIM arity for convenience:

Http.new("url")

GET, URL as envelope

Http.new("url", :tag)

GET, custom envelope

Http.new(:post, "url")

POST, URL as envelope

Http.new(:post, "url", :tag)

POST, custom envelope

Http.new(:post, "url", "body", :tag)

POST with body

Http.new(get: "url")

keyword shortcut

method

HTTP method symbol: :get, :post, :put, :patch, or :delete.

url

Request URL (String).

envelope

Symbol to tag the response message.

headers

Optional hash of HTTP headers.

body

Optional request body (String).

timeout

Optional timeout in seconds (default 10).

parser

Optional callable to transform response body.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/rooibos/command/http.rb', line 75

def new(*args, method: nil, url: nil, envelope: nil, headers: nil, body: nil, timeout: nil, parser: nil,
  get: nil, post: nil, put: nil, patch: nil, delete: nil
)
  # Auto-splat single hash argument
  return new(**args.first) if args.size == 1 && args.first.is_a?(Hash)

  # Auto-spread single array argument
  return new(*args.first) if args.size == 1 && args.first.is_a?(Array)

  # DWIM: parse positional args and keyword method shortcuts
  method_keywords = { get:, post:, put:, patch:, delete: }.compact
  method, url, envelope, body = parse_dwim_args(args, method, url, envelope, body, method_keywords)

  # Ractor validation
  if RatatuiRuby::Debug.enabled? && !Ractor.shareable?(url)
    raise Rooibos::Error::Invariant,
      "URL is not Ractor-shareable: #{url.inspect}\n" \
        "Use a frozen string or Ractor.make_shareable."
  end

  if RatatuiRuby::Debug.enabled? && headers && !Ractor.shareable?(headers)
    raise Rooibos::Error::Invariant,
      "Headers are not Ractor-shareable: #{headers.inspect}\n" \
        "Use Ractor.make_shareable or freeze the hash and its contents."
  end

  if RatatuiRuby::Debug.enabled? && body && !Ractor.shareable?(body)
    raise Rooibos::Error::Invariant,
      "Body is not Ractor-shareable: #{body.inspect}\n" \
        "Use a frozen string or Ractor.make_shareable."
  end

  if RatatuiRuby::Debug.enabled? && envelope && !Ractor.shareable?(envelope)
    raise Rooibos::Error::Invariant,
      "Envelope is not Ractor-shareable: #{envelope.inspect}\n" \
        "Use a frozen string, symbol, or Ractor.make_shareable."
  end

  if RatatuiRuby::Debug.enabled? && timeout && !Ractor.shareable?(timeout)
    raise Rooibos::Error::Invariant,
      "Timeout is not Ractor-shareable: #{timeout.inspect}\n" \
        "Use a number or Ractor.make_shareable."
  end

  # Parser validation
  if parser && !parser.respond_to?(:call)
    raise ArgumentError, "parser: must respond to :call"
  end

  if RatatuiRuby::Debug.enabled? && parser && !Ractor.shareable?(parser)
    raise Rooibos::Error::Invariant,
      "Parser is not Ractor-shareable: #{parser.inspect}\n" \
        "Use a frozen Method object or Ractor.make_shareable."
  end

  # Method validation
  unless %i[get post put patch delete].include?(method)
    raise ArgumentError, "Unsupported HTTP method: #{method.inspect}"
  end

  instance = allocate
  instance.__send__(:initialize, method:, url:, envelope:, headers:, body:, timeout: timeout || 10, parser:)
  instance
end

Instance Method Details

#call(out, token) ⇒ Object

Executes the HTTP request and sends the response.

Sends Message::HttpResponse with status, body, and headers. On network errors, sends the same message type with error populated instead.

Note: Ruby’s Net::HTTP blocks until completion. Cancellation cannot interrupt a request in progress. The grace period is 0.

out

Outlet for sending messages.

token

Cancellation token from the runtime.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/rooibos/command/http.rb', line 199

def call(out, token)
  return if token.canceled?

  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"
  if timeout
    http.open_timeout = timeout
    http.read_timeout = timeout
  end

  klass = case method
          when :get    then Net::HTTP::Get
          when :post   then Net::HTTP::Post
          when :put    then Net::HTTP::Put
          when :patch  then Net::HTTP::Patch
          when :delete then Net::HTTP::Delete
  end
  request = klass.new(uri)
  request.body = body if body
  headers&.each { |key, value| request[key] = value }
  response = http.request(request)

  response_body = response.body.freeze
  response_headers = response.each_header.to_h.freeze
  response_status = response.code.to_i

  # Invoke parser with positional params if provided
  parsed_body = if parser
    parser.call(response_body, response_headers, response_status)
  else
    response_body
  end

  # Validate parsed body is Ractor-shareable in debug mode
  if RatatuiRuby::Debug.enabled? && parser && !Ractor.shareable?(parsed_body)
    raise Rooibos::Error::Invariant,
      "Parsed body is not Ractor-shareable: #{parsed_body.class}\n" \
        "Parser must return frozen/shareable data. Use .freeze or Ractor.make_shareable."
  end

  out.put(Ractor.make_shareable(HttpResponse.new(
    envelope:,
    status: response_status,
    body: parsed_body,
    headers: response_headers,
    error: nil
  )))
rescue => e
  out.put(Ractor.make_shareable(HttpResponse.new(
    envelope:,
    status: nil,
    body: nil,
    headers: nil,
    error: e.message.freeze
  )))
end

#rooibos_cancellation_grace_periodObject

Net::HTTP is blocking; no cooperative cancellation possible. Grace period = 0 means runtime will orphan the blocked thread immediately.



143
# File 'lib/rooibos/command/http.rb', line 143

def rooibos_cancellation_grace_period = 0