Class: Stipa::Response

Inherits:
Object
  • Object
show all
Defined in:
lib/stipa/response.rb

Overview

Builds a valid HTTP/1.1 response and serializes it to wire bytes.

Design notes:

- Content-Length is ALWAYS computed from body.bytesize in to_http,
  never stored manually. This prevents handler authors from setting
  a wrong value and makes binary correctness automatic.
- Header names are stored in Title-Case for wire compatibility but
  set_header accepts any casing for developer ergonomics.
- Keep-alive vs Connection:close is decided in to_http based on the
  request's HTTP version, so handler code never needs to think about it.
- The Date header is injected automatically — required by RFC 7231.

Constant Summary collapse

STATUS_MESSAGES =
{
  200 => 'OK',
  201 => 'Created',
  204 => 'No Content',
  301 => 'Moved Permanently',
  302 => 'Found',
  304 => 'Not Modified',
  400 => 'Bad Request',
  401 => 'Unauthorized',
  403 => 'Forbidden',
  404 => 'Not Found',
  405 => 'Method Not Allowed',
  408 => 'Request Timeout',
  413 => 'Payload Too Large',
  422 => 'Unprocessable Entity',
  429 => 'Too Many Requests',
  500 => 'Internal Server Error',
  502 => 'Bad Gateway',
  503 => 'Service Unavailable',
  504 => 'Gateway Timeout',
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeResponse

Returns a new instance of Response.



41
42
43
44
45
46
# File 'lib/stipa/response.rb', line 41

def initialize
  @status          = 200
  @headers         = {}
  @body            = ''
  @template_engine = nil
end

Instance Attribute Details

#bodyObject

Returns the value of attribute body.



38
39
40
# File 'lib/stipa/response.rb', line 38

def body
  @body
end

#headersObject (readonly)

Returns the value of attribute headers.



39
40
41
# File 'lib/stipa/response.rb', line 39

def headers
  @headers
end

#statusObject

Returns the value of attribute status.



38
39
40
# File 'lib/stipa/response.rb', line 38

def status
  @status
end

#template_engineObject

Returns the value of attribute template_engine.



38
39
40
# File 'lib/stipa/response.rb', line 38

def template_engine
  @template_engine
end

Instance Method Details

#[](name) ⇒ Object



55
56
57
# File 'lib/stipa/response.rb', line 55

def [](name)
  @headers[titlecase(name)]
end

#json(data) ⇒ Object

Set body to a JSON representation of ‘data` and set Content-Type. Returns self so it can be used as the last expression in a handler.



82
83
84
85
86
# File 'lib/stipa/response.rb', line 82

def json(data)
  @body = JSON.generate(data)
  set_header('Content-Type', 'application/json; charset=utf-8')
  self
end

#render(template, locals: {}, layout: :default) ⇒ Object

Render an ERB template and set the body + Content-Type to text/html.

Requires a template engine to be configured on the app:

app = Stipa::App.new(views: 'views')

Examples:

res.render('home')
res.render('users/show', locals: { user: @user })
res.render('welcome',    locals: { name: 'Alice' }, layout: false)
res.render('dashboard',  layout: 'layouts/admin')

Returns self for chaining.



71
72
73
74
75
76
77
78
# File 'lib/stipa/response.rb', line 71

def render(template, locals: {}, layout: :default)
  raise 'No template engine configured. Pass views: "path" to Stipa::App.new.' \
    unless @template_engine

  set_header('Content-Type', 'text/html; charset=utf-8')
  @body = @template_engine.render(template, locals: locals, layout: layout)
  self
end

#set_header(name, value) ⇒ Object Also known as: []=

Set a response header. Name is normalized to Title-Case. Accepts any casing: set_header(‘content-type’, ‘text/html’) is fine.



50
51
52
# File 'lib/stipa/response.rb', line 50

def set_header(name, value)
  @headers[titlecase(name)] = value.to_s
end

#to_http(req = nil) ⇒ Object

Serialize to the exact HTTP/1.1 bytes to write to the socket. ‘req` is used to decide the Connection header (keep-alive or close).



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/stipa/response.rb', line 90

def to_http(req = nil)
  # Force binary encoding so bytesize is always the byte count,
  # not the character count (matters for multi-byte UTF-8 bodies).
  body_bytes = @body.to_s.b

  finalize_headers(body_bytes, req)

  status_text  = STATUS_MESSAGES.fetch(@status, 'Unknown')
  status_line  = "HTTP/1.1 #{@status} #{status_text}"
  header_block = @headers.map { |k, v| "#{k}: #{v}" }.join("\r\n")

  # RFC 7230: blank line (CRLF CRLF) separates header block from body
  "#{status_line}\r\n#{header_block}\r\n\r\n#{body_bytes}"
end