File Coverage

blib/lib/Hypersonic/Response.pm
Criterion Covered Total %
statement 193 237 81.4
branch 34 52 65.3
condition 20 35 57.1
subroutine 43 45 95.5
pod 31 35 88.5
total 321 404 79.4


line stmt bran cond sub pod time code
1             package Hypersonic::Response;
2              
3 3     3   4751 use strict;
  3         9  
  3         130  
4 3     3   17 use warnings;
  3         6  
  3         211  
5 3     3   59 use 5.010;
  3         11  
6              
7             our $VERSION = '0.15';
8              
9             # JIT-compiled Response object using array-based slots for maximum speed
10             # Generates XS accessors at compile time via XS::JIT::Builder
11              
12 3     3   17 use XS::JIT;
  3         10  
  3         81  
13 3     3   14 use XS::JIT::Builder;
  3         45  
  3         291  
14              
15             # Response slots (array indices)
16             use constant {
17 3         459 SLOT_STATUS => 0,
18             SLOT_HEADERS => 1, # HV* of headers
19             SLOT_BODY => 2,
20             SLOT_COOKIES => 3, # AV* of cookie strings
21             SLOT_COUNT => 4,
22 3     3   20 };
  3         7  
23              
24             # Export slot constants for direct access
25 3     3   23 use Exporter 'import';
  3         8  
  3         14414  
26             our @EXPORT_OK = qw(
27             SLOT_STATUS SLOT_HEADERS SLOT_BODY SLOT_COOKIES SLOT_COUNT
28             res
29             );
30              
31             # Shortcut constructor for use in handlers
32 2     2 1 5079 sub res { __PACKAGE__->new(@_) }
33              
34             my $COMPILED = 0;
35             my $MODULE_ID = 0;
36              
37             # Unified compile interface
38             sub compile {
39 0     0 0 0 my ($class, %opts) = @_;
40 0         0 return $class->compile_accessors(%opts);
41             }
42              
43             # Generate and compile XS accessors
44             sub compile_accessors {
45 2     2 0 8 my ($class, %opts) = @_;
46              
47 2 50       5 return 1 if $COMPILED;
48              
49 2   50     16 my $cache_dir = $opts{cache_dir} // '_hypersonic_cache/response';
50 2         5 my $module_name = 'Hypersonic::Response::Accessors_' . $MODULE_ID++;
51              
52 2         27 my $builder = XS::JIT::Builder->new;
53              
54             # Generate read-only accessors for simple slots
55 2         29 $builder->op_ro_accessor('jit_get_status', SLOT_STATUS);
56 2         15 $builder->op_ro_accessor('jit_get_body', SLOT_BODY);
57 2         24 $builder->op_ro_accessor('jit_get_headers_hv', SLOT_HEADERS);
58 2         12 $builder->op_ro_accessor('jit_get_cookies_av', SLOT_COOKIES);
59              
60             # Generate status setter (returns $self for chaining)
61 2         63 $builder->xs_function('jit_set_status')
62             ->xs_preamble
63             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
64             ->if('items > 1')
65             ->line('SvREFCNT_dec(ary[' . SLOT_STATUS . ']);')
66             ->line('ary[' . SLOT_STATUS . '] = newSVsv(ST(1));')
67             ->endif
68             ->line('ST(0) = ST(0);')
69             ->xs_return('1')
70             ->xs_end;
71              
72             # Generate body setter (returns $self for chaining)
73 2         44 $builder->xs_function('jit_set_body')
74             ->xs_preamble
75             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
76             ->if('items > 1')
77             ->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);')
78             ->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));')
79             ->endif
80             ->line('ST(0) = ST(0);')
81             ->xs_return('1')
82             ->xs_end;
83              
84             # Generate single header setter (returns $self for chaining)
85 2         47 $builder->xs_function('jit_set_header')
86             ->xs_preamble
87             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
88             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
89             ->if('items > 2')
90             ->line('STRLEN klen;')
91             ->line('const char* key = SvPV(ST(1), klen);')
92             ->line('SV* val = newSVsv(ST(2));')
93             ->line('hv_store(headers, key, klen, val, 0);')
94             ->endif
95             ->line('ST(0) = ST(0);')
96             ->xs_return('1')
97             ->xs_end;
98              
99             # Generate header getter
100 2         110 $builder->xs_function('jit_get_header')
101             ->xs_preamble
102             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
103             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
104             ->if('items > 1')
105             ->line('STRLEN klen;')
106             ->line('const char* key = SvPV(ST(1), klen);')
107             ->line('SV** val = hv_fetch(headers, key, klen, 0);')
108             ->line('ST(0) = val && *val ? *val : &PL_sv_undef;')
109             ->else
110             ->line('ST(0) = newRV_inc((SV*)headers);')
111             ->endif
112             ->xs_return('1')
113             ->xs_end;
114              
115             # Generate add cookie (pushes to cookies array, returns $self)
116 2         34 $builder->xs_function('jit_add_cookie')
117             ->xs_preamble
118             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
119             ->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);')
120             ->if('items > 1')
121             ->line('av_push(cookies, newSVsv(ST(1)));')
122             ->endif
123             ->line('ST(0) = ST(0);')
124             ->xs_return('1')
125             ->xs_end;
126              
127             # Generate cookies count
128 2         26 $builder->xs_function('jit_cookies_count')
129             ->xs_preamble
130             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
131             ->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);')
132             ->line('ST(0) = sv_2mortal(newSViv(av_len(cookies) + 1));')
133             ->xs_return('1')
134             ->xs_end;
135              
136             # Generate cookie at index
137 2         30 $builder->xs_function('jit_cookie_at')
138             ->xs_preamble
139             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
140             ->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);')
141             ->if('items > 1')
142             ->line('IV idx = SvIV(ST(1));')
143             ->line('SV** val = av_fetch(cookies, idx, 0);')
144             ->line('ST(0) = val && *val ? *val : &PL_sv_undef;')
145             ->else
146             ->line('ST(0) = &PL_sv_undef;')
147             ->endif
148             ->xs_return('1')
149             ->xs_end;
150              
151             # ============================================================
152             # JIT-compiled fluent API methods (return $self for chaining)
153             # ============================================================
154              
155             # JIT text() - sets Content-Type: text/plain + body
156 2         56 $builder->xs_function('jit_text')
157             ->xs_preamble
158             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
159             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
160             ->if('items > 1')
161             ->line('hv_store(headers, "Content-Type", 12, newSVpv("text/plain", 10), 0);')
162             ->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);')
163             ->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));')
164             ->endif
165             ->line('ST(0) = ST(0);')
166             ->xs_return('1')
167             ->xs_end;
168              
169             # JIT html() - sets Content-Type: text/html + body
170 2         27 $builder->xs_function('jit_html')
171             ->xs_preamble
172             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
173             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
174             ->if('items > 1')
175             ->line('hv_store(headers, "Content-Type", 12, newSVpv("text/html", 9), 0);')
176             ->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);')
177             ->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));')
178             ->endif
179             ->line('ST(0) = ST(0);')
180             ->xs_return('1')
181             ->xs_end;
182              
183             # JIT xml() - sets Content-Type: application/xml + body
184 2         45 $builder->xs_function('jit_xml')
185             ->xs_preamble
186             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
187             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
188             ->if('items > 1')
189             ->line('hv_store(headers, "Content-Type", 12, newSVpv("application/xml", 15), 0);')
190             ->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);')
191             ->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));')
192             ->endif
193             ->line('ST(0) = ST(0);')
194             ->xs_return('1')
195             ->xs_end;
196              
197             # JIT content_type() - sets Content-Type header
198 2         33 $builder->xs_function('jit_content_type')
199             ->xs_preamble
200             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
201             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
202             ->if('items > 1')
203             ->line('hv_store(headers, "Content-Type", 12, newSVsv(ST(1)), 0);')
204             ->endif
205             ->line('ST(0) = ST(0);')
206             ->xs_return('1')
207             ->xs_end;
208              
209             # JIT redirect() - sets status + Location header
210 2         47 $builder->xs_function('jit_redirect')
211             ->xs_preamble
212             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
213             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
214             ->if('items > 1')
215             ->line('hv_store(headers, "Location", 8, newSVsv(ST(1)), 0);')
216             ->line('SvREFCNT_dec(ary[' . SLOT_STATUS . ']);')
217             # Check items > 2 AND ST(2) is defined (not undef)
218             ->if('items > 2 && SvOK(ST(2))')
219             ->line('ary[' . SLOT_STATUS . '] = newSVsv(ST(2));')
220             ->else
221             ->line('ary[' . SLOT_STATUS . '] = newSViv(302);')
222             ->endif
223             ->endif
224             ->line('ST(0) = ST(0);')
225             ->xs_return('1')
226             ->xs_end;
227              
228             # ============================================================
229             # JIT-compiled to_http() - direct HTTP response string in C
230             # This is the highest-impact optimization for dynamic routes
231             # ============================================================
232 2         266 $builder->xs_function('jit_to_http')
233             ->xs_preamble
234             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
235             ->line('IV status = SvIV(ary[' . SLOT_STATUS . ']);')
236             ->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);')
237             ->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);')
238             ->line('STRLEN body_len;')
239             ->line('const char* body = SvPV(ary[' . SLOT_BODY . '], body_len);')
240             ->line('')
241             ->line('/* Status line lookup table */')
242             ->line('const char* status_text;')
243             ->line('switch(status) {')
244             ->line(' case 200: status_text = "OK"; break;')
245             ->line(' case 201: status_text = "Created"; break;')
246             ->line(' case 204: status_text = "No Content"; break;')
247             ->line(' case 301: status_text = "Moved Permanently"; break;')
248             ->line(' case 302: status_text = "Found"; break;')
249             ->line(' case 304: status_text = "Not Modified"; break;')
250             ->line(' case 307: status_text = "Temporary Redirect"; break;')
251             ->line(' case 308: status_text = "Permanent Redirect"; break;')
252             ->line(' case 400: status_text = "Bad Request"; break;')
253             ->line(' case 401: status_text = "Unauthorized"; break;')
254             ->line(' case 403: status_text = "Forbidden"; break;')
255             ->line(' case 404: status_text = "Not Found"; break;')
256             ->line(' case 409: status_text = "Conflict"; break;')
257             ->line(' case 422: status_text = "Unprocessable Entity"; break;')
258             ->line(' case 429: status_text = "Too Many Requests"; break;')
259             ->line(' case 500: status_text = "Internal Server Error"; break;')
260             ->line(' case 502: status_text = "Bad Gateway"; break;')
261             ->line(' case 503: status_text = "Service Unavailable"; break;')
262             ->line(' case 504: status_text = "Gateway Timeout"; break;')
263             ->line(' default: status_text = "Unknown"; break;')
264             ->line('}')
265             ->line('')
266             ->line('/* Calculate total size needed */')
267             ->line('SSize_t total_size = 64; /* Status line overhead */')
268             ->line('total_size += body_len;')
269             ->line('total_size += 32; /* Content-Length header */')
270             ->line('')
271             ->line('/* Iterate headers to calculate size */')
272             ->line('HE* entry;')
273             ->line('hv_iterinit(headers);')
274             ->line('while ((entry = hv_iternext(headers))) {')
275             ->line(' I32 klen;')
276             ->line(' const char* key = hv_iterkey(entry, &klen);')
277             ->line(' SV* val = hv_iterval(headers, entry);')
278             ->line(' STRLEN vlen;')
279             ->line(' SvPV(val, vlen);')
280             ->line(' total_size += klen + vlen + 4; /* key: value\\r\\n */')
281             ->line('}')
282             ->line('')
283             ->line('/* Add cookies size */')
284             ->line('SSize_t ncookies = av_len(cookies) + 1;')
285             ->line('for (SSize_t i = 0; i < ncookies; i++) {')
286             ->line(' SV** cv = av_fetch(cookies, i, 0);')
287             ->line(' if (cv && *cv) {')
288             ->line(' STRLEN clen;')
289             ->line(' SvPV(*cv, clen);')
290             ->line(' total_size += 14 + clen + 2; /* Set-Cookie: ... \\r\\n */')
291             ->line(' }')
292             ->line('}')
293             ->line('')
294             ->line('/* Build response string */')
295             ->line('SV* result = newSV(total_size + 4);')
296             ->line('SvPOK_on(result);')
297             ->line('char* p = SvPVX(result);')
298             ->line('')
299             ->line('/* Status line */')
300             ->line('p += sprintf(p, "HTTP/1.1 %d %s\\r\\n", (int)status, status_text);')
301             ->line('')
302             ->line('/* Headers */')
303             ->line('hv_iterinit(headers);')
304             ->line('while ((entry = hv_iternext(headers))) {')
305             ->line(' I32 klen;')
306             ->line(' const char* key = hv_iterkey(entry, &klen);')
307             ->line(' SV* val = hv_iterval(headers, entry);')
308             ->line(' STRLEN vlen;')
309             ->line(' const char* vp = SvPV(val, vlen);')
310             ->line(' memcpy(p, key, klen); p += klen;')
311             ->line(' *p++ = \':\'; *p++ = \' \';')
312             ->line(' memcpy(p, vp, vlen); p += vlen;')
313             ->line(' *p++ = \'\\r\'; *p++ = \'\\n\';')
314             ->line('}')
315             ->line('')
316             ->line('/* Cookies */')
317             ->line('for (SSize_t i = 0; i < ncookies; i++) {')
318             ->line(' SV** cv = av_fetch(cookies, i, 0);')
319             ->line(' if (cv && *cv) {')
320             ->line(' STRLEN clen;')
321             ->line(' const char* cookie = SvPV(*cv, clen);')
322             ->line(' memcpy(p, "Set-Cookie: ", 12); p += 12;')
323             ->line(' memcpy(p, cookie, clen); p += clen;')
324             ->line(' *p++ = \'\\r\'; *p++ = \'\\n\';')
325             ->line(' }')
326             ->line('}')
327             ->line('')
328             ->line('/* Content-Length and blank line */')
329             ->line('p += sprintf(p, "Content-Length: %lu\\r\\n\\r\\n", (unsigned long)body_len);')
330             ->line('')
331             ->line('/* Body */')
332             ->line('memcpy(p, body, body_len);')
333             ->line('p += body_len;')
334             ->line('')
335             ->line('SvCUR_set(result, p - SvPVX(result));')
336             ->line('ST(0) = sv_2mortal(result);')
337             ->xs_return('1')
338             ->xs_end;
339              
340             # ============================================================
341             # JIT-compiled HTTP date formatting (for Last-Modified, etc.)
342             # ============================================================
343 2         10 $builder->line('#include ')
344             ->blank;
345              
346 2         45 $builder->xs_function('jit_http_date')
347             ->xs_preamble
348             ->line('time_t t = items > 0 && SvOK(ST(0)) ? (time_t)SvIV(ST(0)) : time(NULL);')
349             ->line('struct tm* gm = gmtime(&t);')
350             ->blank
351             ->line('static const char* days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};')
352             ->line('static const char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};')
353             ->blank
354             ->line('char buf[32];')
355             ->line('int len = sprintf(buf, "%s, %02d %s %04d %02d:%02d:%02d GMT",')
356             ->line(' days[gm->tm_wday], gm->tm_mday, months[gm->tm_mon],')
357             ->line(' gm->tm_year + 1900, gm->tm_hour, gm->tm_min, gm->tm_sec);')
358             ->blank
359             ->line('ST(0) = sv_2mortal(newSVpv(buf, len));')
360             ->xs_return('1')
361             ->xs_end;
362              
363             # Compile via XS::JIT
364 2         1314124 XS::JIT->compile(
365             code => $builder->code,
366             name => $module_name,
367             cache_dir => $cache_dir,
368             functions => {
369             # Status
370             'Hypersonic::Response::_jit_get_status' => { source => 'jit_get_status', is_xs_native => 1 },
371             'Hypersonic::Response::_jit_set_status' => { source => 'jit_set_status', is_xs_native => 1 },
372              
373             # Body
374             'Hypersonic::Response::_jit_get_body' => { source => 'jit_get_body', is_xs_native => 1 },
375             'Hypersonic::Response::_jit_set_body' => { source => 'jit_set_body', is_xs_native => 1 },
376              
377             # Headers
378             'Hypersonic::Response::_jit_get_headers_hv' => { source => 'jit_get_headers_hv', is_xs_native => 1 },
379             'Hypersonic::Response::_jit_set_header' => { source => 'jit_set_header', is_xs_native => 1 },
380             'Hypersonic::Response::_jit_get_header' => { source => 'jit_get_header', is_xs_native => 1 },
381              
382             # Cookies
383             'Hypersonic::Response::_jit_get_cookies_av' => { source => 'jit_get_cookies_av', is_xs_native => 1 },
384             'Hypersonic::Response::_jit_add_cookie' => { source => 'jit_add_cookie', is_xs_native => 1 },
385             'Hypersonic::Response::_jit_cookies_count' => { source => 'jit_cookies_count', is_xs_native => 1 },
386             'Hypersonic::Response::_jit_cookie_at' => { source => 'jit_cookie_at', is_xs_native => 1 },
387              
388             # Fluent API methods (JIT-compiled)
389             'Hypersonic::Response::_jit_text' => { source => 'jit_text', is_xs_native => 1 },
390             'Hypersonic::Response::_jit_html' => { source => 'jit_html', is_xs_native => 1 },
391             'Hypersonic::Response::_jit_xml' => { source => 'jit_xml', is_xs_native => 1 },
392             'Hypersonic::Response::_jit_content_type' => { source => 'jit_content_type', is_xs_native => 1 },
393             'Hypersonic::Response::_jit_redirect' => { source => 'jit_redirect', is_xs_native => 1 },
394              
395             # Direct HTTP response generation
396             'Hypersonic::Response::_jit_to_http' => { source => 'jit_to_http', is_xs_native => 1 },
397              
398             # Utility functions
399             'Hypersonic::Response::_jit_http_date' => { source => 'jit_http_date', is_xs_native => 1 },
400             },
401             );
402              
403 2         66 $COMPILED = 1;
404 2         30 return 1;
405             }
406              
407             # Constructor - creates array-based response object
408             sub new {
409 85     85 1 327629 my ($class, %opts) = @_;
410              
411             # Compile accessors if not already done
412 85 100       224 $class->compile_accessors(cache_dir => $opts{cache_dir}) unless $COMPILED;
413              
414 85         158 my $self = bless [], $class;
415              
416             # Initialize slots
417 85   100     396 $self->[SLOT_STATUS] = $opts{status} // 200;
418 85   100     311 $self->[SLOT_HEADERS] = $opts{headers} // {};
419 85   100     215 $self->[SLOT_BODY] = $opts{body} // '';
420 85         110 $self->[SLOT_COOKIES] = [];
421              
422 85         303 return $self;
423             }
424              
425             # Fluent API methods - all return $self for chaining
426             # Core methods use JIT-compiled XS for maximum speed when available
427              
428             # Set HTTP status code (JIT-compiled when available)
429             sub status {
430 29     29 1 1552 my ($self, $code) = @_;
431 29 50 33     122 if ($COMPILED && defined $code) {
432 29         146 return $self->_jit_set_status($code);
433             }
434 0 0       0 $self->[SLOT_STATUS] = $code if defined $code;
435 0         0 return $self;
436             }
437              
438             # Set a response header (JIT-compiled when available)
439             sub header {
440 9     9 1 1086 my ($self, $name, $value) = @_;
441 9 50       18 if ($COMPILED) {
442 9         44 return $self->_jit_set_header($name, $value);
443             }
444 0         0 $self->[SLOT_HEADERS]{$name} = $value;
445 0         0 return $self;
446             }
447              
448             # Set multiple headers at once
449             sub headers {
450 2     2 1 1945 my ($self, %headers) = @_;
451 2 50       5 if ($COMPILED) {
452 2         15 $self->_jit_set_header($_, $headers{$_}) for keys %headers;
453             } else {
454 0         0 $self->[SLOT_HEADERS]{$_} = $headers{$_} for keys %headers;
455             }
456 2         4 return $self;
457             }
458              
459             # Set response body (JIT-compiled when available)
460             sub body {
461 21     21 1 1223 my ($self, $content) = @_;
462 21 50       32 if ($COMPILED) {
463 21         64 return $self->_jit_set_body($content);
464             }
465 0         0 $self->[SLOT_BODY] = $content;
466 0         0 return $self;
467             }
468              
469             # Set JSON response (auto-sets Content-Type and encodes data)
470             sub json {
471 11     11 1 21 my ($self, $data) = @_;
472 11         1380 require Cpanel::JSON::XS;
473 11         4330 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
474 11         88 $self->[SLOT_BODY] = Cpanel::JSON::XS::encode_json($data);
475 11         29 return $self;
476             }
477              
478             # Set plain text response (JIT-compiled when available)
479             sub text {
480 7     7 1 22 my ($self, $content) = @_;
481 7 50       30 if ($COMPILED) {
482 7         188 return $self->_jit_text($content);
483             }
484 0         0 $self->[SLOT_HEADERS]{'Content-Type'} = 'text/plain';
485 0         0 $self->[SLOT_BODY] = $content;
486 0         0 return $self;
487             }
488              
489             # Set HTML response (JIT-compiled when available)
490             sub html {
491 1     1 1 9 my ($self, $content) = @_;
492 1 50       15 if ($COMPILED) {
493 1         11 return $self->_jit_html($content);
494             }
495 0         0 $self->[SLOT_HEADERS]{'Content-Type'} = 'text/html';
496 0         0 $self->[SLOT_BODY] = $content;
497 0         0 return $self;
498             }
499              
500             # Set XML response (JIT-compiled when available)
501             sub xml {
502 1     1 1 5 my ($self, $content) = @_;
503 1 50       3 if ($COMPILED) {
504 1         43 return $self->_jit_xml($content);
505             }
506 0         0 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/xml';
507 0         0 $self->[SLOT_BODY] = $content;
508 0         0 return $self;
509             }
510              
511             # Set redirect response (JIT-compiled when available)
512             sub redirect {
513 3     3 1 14 my ($self, $url, $code) = @_;
514 3 50       6 if ($COMPILED) {
515 3         16 return $self->_jit_redirect($url, $code);
516             }
517 0   0     0 $self->[SLOT_STATUS] = $code // 302;
518 0         0 $self->[SLOT_HEADERS]{'Location'} = $url;
519 0         0 return $self;
520             }
521              
522             # Set a cookie
523             sub cookie {
524 18     18 1 1826 my ($self, $name, $value, %opts) = @_;
525              
526 18         36 my $cookie = "$name=$value";
527 18 100       48 $cookie .= "; Path=$opts{path}" if defined $opts{path};
528 18 100       41 $cookie .= "; Domain=$opts{domain}" if defined $opts{domain};
529 18 100       48 $cookie .= "; Max-Age=$opts{max_age}" if defined $opts{max_age};
530 18 100       41 $cookie .= "; Expires=$opts{expires}" if defined $opts{expires};
531 18 100       41 $cookie .= "; HttpOnly" if $opts{httponly};
532 18 100       107 $cookie .= "; Secure" if $opts{secure};
533 18 100       44 $cookie .= "; SameSite=$opts{samesite}" if defined $opts{samesite};
534              
535 18         24 push @{$self->[SLOT_COOKIES]}, $cookie;
  18         44  
536 18         46 return $self;
537             }
538              
539             # Clear a cookie (set to expire immediately)
540             sub clear_cookie {
541 2     2 1 9 my ($self, $name, %opts) = @_;
542 2         8 return $self->cookie($name, '', max_age => 0, %opts);
543             }
544              
545             # Set Content-Type header directly (JIT-compiled when available)
546             sub content_type {
547 4     4 1 35 my ($self, $type) = @_;
548 4 50       68 if ($COMPILED) {
549 4         27 return $self->_jit_content_type($type);
550             }
551 0         0 $self->[SLOT_HEADERS]{'Content-Type'} = $type;
552 0         0 return $self;
553             }
554              
555             # Set Cache-Control header
556             sub cache {
557 1     1 1 6 my ($self, $directive) = @_;
558 1         4 $self->[SLOT_HEADERS]{'Cache-Control'} = $directive;
559 1         2 return $self;
560             }
561              
562             # Disable caching
563             sub no_cache {
564 1     1 1 9 my ($self) = @_;
565 1         3 $self->[SLOT_HEADERS]{'Cache-Control'} = 'no-store, no-cache, must-revalidate';
566 1         2 $self->[SLOT_HEADERS]{'Pragma'} = 'no-cache';
567 1         3 return $self;
568             }
569              
570             # Set ETag header
571             sub etag {
572 1     1 1 7 my ($self, $value) = @_;
573 1         5 $self->[SLOT_HEADERS]{'ETag'} = qq("$value");
574 1         2 return $self;
575             }
576              
577             # Set Last-Modified header
578             sub last_modified {
579 1     1 1 7 my ($self, $time) = @_;
580 1         6 $self->[SLOT_HEADERS]{'Last-Modified'} = _http_date($time);
581 1         4 return $self;
582             }
583              
584             # Set Content-Disposition for file downloads
585             sub attachment {
586 1     1 1 7 my ($self, $filename) = @_;
587 1 50       3 if (defined $filename) {
588 1         5 $self->[SLOT_HEADERS]{'Content-Disposition'} = qq(attachment; filename="$filename");
589             } else {
590 0         0 $self->[SLOT_HEADERS]{'Content-Disposition'} = 'attachment';
591             }
592 1         2 return $self;
593             }
594              
595             # Convenience: 201 Created with optional Location header
596             sub created {
597 1     1 1 7 my ($self, $location) = @_;
598 1         3 $self->[SLOT_STATUS] = 201;
599 1 50       5 $self->[SLOT_HEADERS]{'Location'} = $location if defined $location;
600 1         2 return $self;
601             }
602              
603             # Convenience: 204 No Content
604             sub no_content {
605 1     1 1 7 my ($self) = @_;
606 1         2 $self->[SLOT_STATUS] = 204;
607 1         2 $self->[SLOT_BODY] = '';
608 1         2 return $self;
609             }
610              
611             # Convenience: 400 Bad Request
612             sub bad_request {
613 3     3 1 16 my ($self, $message) = @_;
614 3         6 $self->[SLOT_STATUS] = 400;
615 3         7 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
616 3   50     13 $self->[SLOT_BODY] = _json_error($message // 'Bad Request');
617 3         5 return $self;
618             }
619              
620             # Convenience: 401 Unauthorized
621             sub unauthorized {
622 2     2 1 11 my ($self, $message) = @_;
623 2         5 $self->[SLOT_STATUS] = 401;
624 2         5 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
625 2   100     10 $self->[SLOT_BODY] = _json_error($message // 'Unauthorized');
626 2         5 return $self;
627             }
628              
629             # Convenience: 403 Forbidden
630             sub forbidden {
631 2     2 1 15 my ($self, $message) = @_;
632 2         6 $self->[SLOT_STATUS] = 403;
633 2         6 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
634 2   100     13 $self->[SLOT_BODY] = _json_error($message // 'Forbidden');
635 2         5 return $self;
636             }
637              
638             # Convenience: 404 Not Found
639             sub not_found {
640 2     2 1 12 my ($self, $message) = @_;
641 2         5 $self->[SLOT_STATUS] = 404;
642 2         7 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
643 2   100     11 $self->[SLOT_BODY] = _json_error($message // 'Not Found');
644 2         5 return $self;
645             }
646              
647             # Convenience: 409 Conflict
648             sub conflict {
649 1     1 1 7 my ($self, $message) = @_;
650 1         3 $self->[SLOT_STATUS] = 409;
651 1         3 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
652 1   50     5 $self->[SLOT_BODY] = _json_error($message // 'Conflict');
653 1         2 return $self;
654             }
655              
656             # Convenience: 422 Unprocessable Entity
657             sub unprocessable {
658 1     1 1 6 my ($self, $message) = @_;
659 1         1 $self->[SLOT_STATUS] = 422;
660 1         3 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
661 1   50     4 $self->[SLOT_BODY] = _json_error($message // 'Unprocessable Entity');
662 1         2 return $self;
663             }
664              
665             # Convenience: 429 Too Many Requests
666             sub too_many_requests {
667 1     1 1 5 my ($self, $retry_after) = @_;
668 1         1 $self->[SLOT_STATUS] = 429;
669 1         4 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
670 1 50       3 $self->[SLOT_HEADERS]{'Retry-After'} = $retry_after if defined $retry_after;
671 1         3 $self->[SLOT_BODY] = _json_error('Too Many Requests');
672 1         3 return $self;
673             }
674              
675             # Convenience: 500 Internal Server Error
676             sub server_error {
677 2     2 1 12 my ($self, $message) = @_;
678 2         5 $self->[SLOT_STATUS] = 500;
679 2         4 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
680 2   100     11 $self->[SLOT_BODY] = _json_error($message // 'Internal Server Error');
681 2         5 return $self;
682             }
683              
684             # Convenience: 503 Service Unavailable
685             sub unavailable {
686 1     1 1 5 my ($self, $retry_after) = @_;
687 1         2 $self->[SLOT_STATUS] = 503;
688 1         3 $self->[SLOT_HEADERS]{'Content-Type'} = 'application/json';
689 1 50       3 $self->[SLOT_HEADERS]{'Retry-After'} = $retry_after if defined $retry_after;
690 1         2 $self->[SLOT_BODY] = _json_error('Service Unavailable');
691 1         2 return $self;
692             }
693              
694             # Convert to hashref for Hypersonic handler return
695             sub finalize {
696 6     6 1 4475 my ($self) = @_;
697              
698 6         10 my $headers = { %{$self->[SLOT_HEADERS]} };
  6         15  
699              
700             # Add Set-Cookie headers
701 6 100       7 if (@{$self->[SLOT_COOKIES]}) {
  6         17  
702             # Store cookies as arrayref for multiple Set-Cookie headers
703 1         4 $headers->{'Set-Cookie'} = $self->[SLOT_COOKIES];
704             }
705              
706             return {
707 6         22 status => $self->[SLOT_STATUS],
708             headers => $headers,
709             body => $self->[SLOT_BODY],
710             };
711             }
712              
713             # Direct HTTP response string generation (JIT-compiled when available)
714             # This bypasses the intermediate hashref for maximum performance
715             # Returns a complete HTTP response string ready to send
716             sub to_http {
717 29     29 0 91 my ($self) = @_;
718 29 50       57 if ($COMPILED) {
719 29         215 return $self->_jit_to_http();
720             }
721             # Fallback to Perl implementation
722 0         0 return $self->_perl_to_http();
723             }
724              
725             # Pure Perl fallback for to_http
726             sub _perl_to_http {
727 0     0   0 my ($self) = @_;
728              
729 0         0 my $status = $self->[SLOT_STATUS];
730 0         0 my %status_text = (
731             200 => 'OK', 201 => 'Created', 204 => 'No Content',
732             301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified',
733             307 => 'Temporary Redirect', 308 => 'Permanent Redirect',
734             400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
735             404 => 'Not Found', 409 => 'Conflict', 422 => 'Unprocessable Entity',
736             429 => 'Too Many Requests', 500 => 'Internal Server Error',
737             502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout',
738             );
739              
740 0   0     0 my $resp = "HTTP/1.1 $status " . ($status_text{$status} // 'Unknown') . "\r\n";
741              
742             # Headers
743 0         0 my $headers = $self->[SLOT_HEADERS];
744 0         0 for my $key (keys %$headers) {
745 0         0 $resp .= "$key: $headers->{$key}\r\n";
746             }
747              
748             # Cookies
749 0         0 for my $cookie (@{$self->[SLOT_COOKIES]}) {
  0         0  
750 0         0 $resp .= "Set-Cookie: $cookie\r\n";
751             }
752              
753             # Content-Length and body
754 0         0 my $body = $self->[SLOT_BODY];
755 0         0 $resp .= "Content-Length: " . length($body) . "\r\n\r\n";
756 0         0 $resp .= $body;
757              
758 0         0 return $resp;
759             }
760              
761             # Allow using Response object directly as return value (auto-finalize)
762             sub TO_JSON {
763 1     1 0 6 my ($self) = @_;
764 1         4 return $self->finalize;
765             }
766              
767             # Helper: format HTTP date
768             sub _http_date {
769 1     1   4 my ($time) = @_;
770              
771             # Use JIT-compiled version if available
772 1 50 33     7 if ($COMPILED && defined &_jit_http_date) {
773 1         73 return _jit_http_date($time);
774             }
775              
776             # Perl fallback
777 0   0     0 $time //= time();
778 0         0 my @t = gmtime($time);
779 0         0 my @days = qw(Sun Mon Tue Wed Thu Fri Sat);
780 0         0 my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
781 0         0 return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",
782             $days[$t[6]], $t[3], $months[$t[4]], $t[5]+1900, $t[2], $t[1], $t[0]);
783             }
784              
785             # Helper: format JSON error
786             sub _json_error {
787 15     15   28 my ($message) = @_;
788 15         38 $message =~ s/"/\\"/g; # Escape quotes
789 15         38 return qq({"error":"$message"});
790             }
791              
792             1;
793              
794             __END__