Class: Tina4::Response

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

Constant Summary collapse

MIME_TYPES =
{
  ".html" => "text/html", ".htm" => "text/html",
  ".css" => "text/css", ".js" => "application/javascript",
  ".json" => "application/json", ".xml" => "application/xml",
  ".txt" => "text/plain", ".csv" => "text/csv",
  ".png" => "image/png", ".jpg" => "image/jpeg",
  ".jpeg" => "image/jpeg", ".gif" => "image/gif",
  ".svg" => "image/svg+xml", ".ico" => "image/x-icon",
  ".webp" => "image/webp", ".pdf" => "application/pdf",
  ".zip" => "application/zip", ".woff" => "font/woff",
  ".woff2" => "font/woff2", ".ttf" => "font/ttf",
  ".eot" => "application/vnd.ms-fontobject",
  ".mp3" => "audio/mpeg", ".mp4" => "video/mp4",
  ".webm" => "video/webm"
}.freeze
JSON_CONTENT_TYPE =

Pre-frozen header values

"application/json; charset=utf-8"
HTML_CONTENT_TYPE =
"text/html; charset=utf-8"
TEXT_CONTENT_TYPE =
"text/plain; charset=utf-8"
XML_CONTENT_TYPE =
"application/xml; charset=utf-8"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeResponse

Returns a new instance of Response.



31
32
33
34
35
36
# File 'lib/tina4/response.rb', line 31

def initialize
  @status_code = 200
  @headers = { "content-type" => HTML_CONTENT_TYPE }
  @body = ""
  @cookies = nil  # Lazy -- most responses have no cookies
end

Instance Attribute Details

#bodyObject

Returns the value of attribute body.



29
30
31
# File 'lib/tina4/response.rb', line 29

def body
  @body
end

#cookiesObject

Returns the value of attribute cookies.



29
30
31
# File 'lib/tina4/response.rb', line 29

def cookies
  @cookies
end

#headersObject

Returns the value of attribute headers.



29
30
31
# File 'lib/tina4/response.rb', line 29

def headers
  @headers
end

#status_codeObject

Returns the value of attribute status_code.



29
30
31
# File 'lib/tina4/response.rb', line 29

def status_code
  @status_code
end

Class Method Details

.auto_detect(result, response) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/tina4/response.rb', line 255

def self.auto_detect(result, response)
  case result
  when Tina4::Response
    result
  when Hash, Array
    response.json(result)
  when String
    if result.start_with?("<")
      response.html(result)
    else
      response.text(result)
    end
  when Integer
    response.status_code = result
    response.body = ""
    response
  when NilClass
    response.status_code = 204
    response.body = ""
    response
  else
    response.json(result.respond_to?(:to_hash) ? result.to_hash : { data: result.to_s })
  end
end

.error_envelope(code, message, status = 400) ⇒ Object

Build a standard error envelope hash (class method).

Usage:

response.json(Tina4::Response.error_envelope("NOT_FOUND", "Resource not found", 404), status: 404)


157
158
159
# File 'lib/tina4/response.rb', line 157

def self.error_envelope(code, message, status = 400)
  { error: true, code: code, message: message, status: status }
end

Instance Method Details

#add_cors_headers(origin: "*", methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS", headers_list: "Content-Type, Authorization, Accept", credentials: false) ⇒ Object



198
199
200
201
202
203
204
205
206
# File 'lib/tina4/response.rb', line 198

def add_cors_headers(origin: "*", methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
                     headers_list: "Content-Type, Authorization, Accept", credentials: false)
  @headers["access-control-allow-origin"] = origin
  @headers["access-control-allow-methods"] = methods
  @headers["access-control-allow-headers"] = headers_list
  @headers["access-control-allow-credentials"] = "true" if credentials
  @headers["access-control-max-age"] = "86400"
  self
end

#add_header(key, value) ⇒ Object



193
194
195
196
# File 'lib/tina4/response.rb', line 193

def add_header(key, value)
  @headers[key] = value
  self
end

#call(data = nil, status_code = 200, content_type = nil) ⇒ Object

Callable response — auto-detects content type from data. Matches Python __call__ / PHP __invoke / Node response() pattern.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/tina4/response.rb', line 50

def call(data = nil, status_code = 200, content_type = nil)
  @status_code = status_code
  if content_type
    @headers["content-type"] = content_type
    @body = data.to_s
  elsif data.is_a?(Hash) || data.is_a?(Array)
    @headers["content-type"] = JSON_CONTENT_TYPE
    @body = JSON.generate(data)
  else
    @headers["content-type"] = HTML_CONTENT_TYPE
    @body = data.to_s
  end
  self
end

Chainable cookie setter



172
173
174
# File 'lib/tina4/response.rb', line 172

def cookie(name, value, opts = {})
  set_cookie(name, value, opts)
end

#csv(content, filename: "export.csv", status: 200) ⇒ Object



93
94
95
96
97
98
99
# File 'lib/tina4/response.rb', line 93

def csv(content, filename: "export.csv", status: 200)
  @status_code = status
  @headers["content-type"] = "text/csv"
  @headers["content-disposition"] = "attachment; filename=\"#{filename}\""
  @body = content.to_s
  self
end


189
190
191
# File 'lib/tina4/response.rb', line 189

def delete_cookie(name, path: "/")
  set_cookie(name, "", max_age: 0, path: path)
end

#error(code, message, status_code = 400) ⇒ Object

Standard error response envelope.

Usage:

response.error("VALIDATION_FAILED", "Email is required", 400)


140
141
142
143
144
145
146
147
148
149
150
# File 'lib/tina4/response.rb', line 140

def error(code, message, status_code = 400)
  @status_code = status_code
  @headers["content-type"] = JSON_CONTENT_TYPE
  @body = JSON.generate({
    error: true,
    code: code,
    message: message,
    status: status_code
  })
  self
end

#file(path, content_type: nil, download: false) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/tina4/response.rb', line 108

def file(path, content_type: nil, download: false)
  unless ::File.exist?(path)
    @status_code = 404
    @body = "File not found"
    return self
  end
  ext = ::File.extname(path).downcase
  @headers["content-type"] = content_type || MIME_TYPES[ext] || "application/octet-stream"
  if download
    @headers["content-disposition"] = "attachment; filename=\"#{::File.basename(path)}\""
  end
  @body = ::File.binread(path)
  self
end

#header(name, value = nil) ⇒ Object

Chainable header setter



162
163
164
165
166
167
168
169
# File 'lib/tina4/response.rb', line 162

def header(name, value = nil)
  if value.nil?
    @headers[name]
  else
    @headers[name] = value
    self
  end
end

#html(content, status_or_opts = nil, status: nil) ⇒ Object



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

def html(content, status_or_opts = nil, status: nil)
  @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
  @headers["content-type"] = HTML_CONTENT_TYPE
  @body = content.to_s
  self
end

#json(data, status_or_opts = nil, status: nil) ⇒ Object



65
66
67
68
69
70
# File 'lib/tina4/response.rb', line 65

def json(data, status_or_opts = nil, status: nil)
  @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
  @headers["content-type"] = JSON_CONTENT_TYPE
  @body = data.is_a?(String) ? data : JSON.generate(data)
  self
end

#redirect(url, status_or_opts = nil, status: nil) ⇒ Object



101
102
103
104
105
106
# File 'lib/tina4/response.rb', line 101

def redirect(url, status_or_opts = nil, status: nil)
  @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 302)
  @headers["location"] = url
  @body = ""
  self
end

#render(template_path, data = {}, status: 200, template_dir: nil) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
# File 'lib/tina4/response.rb', line 123

def render(template_path, data = {}, status: 200, template_dir: nil)
  @status_code = status
  @headers["content-type"] = HTML_CONTENT_TYPE
  if template_dir
    frond = Tina4::Frond.new(template_dir: template_dir)
    @body = frond.render(template_path, data)
  else
    @body = Tina4::Template.render(template_path, data)
  end
  self
end

#sendObject

Flush / finalize – alias for to_rack for semantic clarity



235
236
237
# File 'lib/tina4/response.rb', line 235

def send
  to_rack
end


176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/tina4/response.rb', line 176

def set_cookie(name, value, opts = {})
  cookie_str = "#{name}=#{URI.encode_www_form_component(value)}"
  cookie_str += "; Path=#{opts[:path] || '/'}"
  cookie_str += "; HttpOnly" if opts.fetch(:http_only, true)
  cookie_str += "; Secure" if opts[:secure]
  cookie_str += "; SameSite=#{opts[:same_site] || 'Lax'}"
  cookie_str += "; Max-Age=#{opts[:max_age]}" if opts[:max_age]
  cookie_str += "; Expires=#{opts[:expires].httpdate}" if opts[:expires]
  @cookies ||= []
  @cookies << cookie_str
  self
end

#status(code = nil) ⇒ Object

Chainable status setter



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

def status(code = nil)
  if code.nil?
    @status_code
  else
    @status_code = code
    self
  end
end

#stream(content_type: "text/event-stream") {|Enumerator::Yielder| ... } ⇒ self

Stream response from a block for Server-Sent Events (SSE).

Usage:

Tina4::Router.get "/events" do |request, response|
  response.stream do |out|
    10.times do |i|
      out << "data: message #{i}\n\n"
      sleep 1
    end
  end
end

Parameters:

  • content_type (String) (defaults to: "text/event-stream")

    Content type (default: text/event-stream)

Yields:

  • (Enumerator::Yielder)

    Block receives a yielder to push chunks

Returns:

  • (self)


223
224
225
226
227
228
229
230
231
232
# File 'lib/tina4/response.rb', line 223

def stream(content_type: "text/event-stream", &block)
  @status_code = @status_code || 200
  @headers["content-type"] = content_type
  @headers["cache-control"] = "no-cache"
  @headers["connection"] = "keep-alive"
  @headers["x-accel-buffering"] = "no"
  @_streaming = true
  @_stream_block = block
  self
end

#text(content, status_or_opts = nil, status: nil) ⇒ Object



79
80
81
82
83
84
# File 'lib/tina4/response.rb', line 79

def text(content, status_or_opts = nil, status: nil)
  @status_code = status || (status_or_opts.is_a?(Integer) ? status_or_opts : 200)
  @headers["content-type"] = TEXT_CONTENT_TYPE
  @body = content.to_s
  self
end

#to_rackObject



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/tina4/response.rb', line 239

def to_rack
  final_headers = @headers.dup
  final_headers["set-cookie"] = @cookies.join("\n") if @cookies && !@cookies.empty?

  if @_streaming
    # Streaming mode — return an Enumerator as the body
    body = Enumerator.new do |yielder|
      @_stream_block.call(yielder)
    end
    return [@status_code, final_headers, body]
  end

  # Normal buffered response
  [@status_code, final_headers, [@body.to_s]]
end

#xml(content, status: 200) ⇒ Object



86
87
88
89
90
91
# File 'lib/tina4/response.rb', line 86

def xml(content, status: 200)
  @status_code = status
  @headers["content-type"] = XML_CONTENT_TYPE
  @body = content.to_s
  self
end