mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00

Implement `/foo/1/edit` route/view. Allow setting HTTP verb by passing e.g. `/_PATCH` as the last segment of a URL - this allows browsers to submit a `POST` request with a pseudo-HTTP verb encoded in the URL which Jetzig can translate, i.e. allowing forms to submit a `PATCH`.
819 lines
30 KiB
Zig
819 lines
30 KiB
Zig
const std = @import("std");
|
|
|
|
const httpz = @import("httpz");
|
|
|
|
const jetzig = @import("../../jetzig.zig");
|
|
|
|
const Request = @This();
|
|
const default_content_type = "text/html";
|
|
|
|
pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS, TRACE };
|
|
pub const Modifier = enum { edit, new };
|
|
pub const Format = enum { HTML, JSON, UNKNOWN };
|
|
pub const Protocol = enum { http, https };
|
|
pub const RequestState = enum {
|
|
initial, // No processing has taken place
|
|
processed, // Request headers have been processed
|
|
after_request, // Initial middleware processing
|
|
rendered, // Rendered by middleware or view
|
|
redirected, // Redirected by middleware or view
|
|
failed, // Failed by middleware or view
|
|
before_response, // Post middleware processing
|
|
finalized, // Response generated
|
|
};
|
|
|
|
allocator: std.mem.Allocator,
|
|
path: jetzig.http.Path,
|
|
method: Method,
|
|
headers: jetzig.http.Headers,
|
|
server: *jetzig.http.Server,
|
|
httpz_request: *httpz.Request,
|
|
httpz_response: *httpz.Response,
|
|
response: *jetzig.http.Response,
|
|
status_code: jetzig.http.status_codes.StatusCode = .not_found,
|
|
response_data: *jetzig.data.Data,
|
|
query_params: ?*jetzig.http.Query = null,
|
|
query_body: ?*jetzig.http.Query = null,
|
|
_params_info: ?jetzig.http.params.ParamsInfo = null,
|
|
multipart: ?jetzig.http.MultipartQuery = null,
|
|
parsed_multipart: ?*jetzig.data.Data = null,
|
|
_cookies: ?*jetzig.http.Cookies = null,
|
|
_session: ?*jetzig.http.Session = null,
|
|
body: []const u8 = undefined,
|
|
state: RequestState = .initial,
|
|
dynamic_assigned_template: ?[]const u8 = null,
|
|
layout: ?[]const u8 = null,
|
|
layout_disabled: bool = false,
|
|
redirect_state: ?RedirectState = null,
|
|
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
|
|
middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
|
|
rendered_multiple: bool = false,
|
|
rendered_view: ?jetzig.views.View = null,
|
|
start_time: i128,
|
|
store: RequestStore,
|
|
cache: RequestStore,
|
|
repo: *jetzig.database.Repo,
|
|
global: *jetzig.Global,
|
|
|
|
/// Wrapper for KV store that uses the request's arena allocator for fetching values.
|
|
pub const RequestStore = struct {
|
|
allocator: std.mem.Allocator,
|
|
store: *jetzig.kv.Store,
|
|
|
|
/// Put a String or into the key-value store.
|
|
pub fn get(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
|
|
return try self.store.get(try self.data(), key);
|
|
}
|
|
|
|
/// Get a String from the store.
|
|
pub fn put(self: RequestStore, key: []const u8, value: anytype) !void {
|
|
const alloc = (try self.data()).allocator();
|
|
try self.store.put(key, try jetzig.Data.zmplValue(value, alloc));
|
|
}
|
|
|
|
/// Remove a String to from the key-value store and return it if found.
|
|
pub fn fetchRemove(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
|
|
return try self.store.fetchRemove(try self.data(), key);
|
|
}
|
|
|
|
/// Remove a String to from the key-value store.
|
|
pub fn remove(self: RequestStore, key: []const u8) !void {
|
|
try self.store.remove(key);
|
|
}
|
|
|
|
/// Append a Value to the end of an Array in the key-value store.
|
|
pub fn append(self: RequestStore, key: []const u8, value: anytype) !void {
|
|
const alloc = (try self.data()).allocator();
|
|
try self.store.append(key, try jetzig.Data.zmplValue(value, alloc));
|
|
}
|
|
|
|
/// Prepend a Value to the start of an Array in the key-value store.
|
|
pub fn prepend(self: RequestStore, key: []const u8, value: anytype) !void {
|
|
const alloc = (try self.data()).allocator();
|
|
try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc));
|
|
}
|
|
|
|
/// Pop a String from an Array in the key-value store.
|
|
pub fn pop(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
|
|
return try self.store.pop(try self.data(), key);
|
|
}
|
|
|
|
/// Left-pop a String from an Array in the key-value store.
|
|
pub fn popFirst(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
|
|
return try self.store.popFirst(try self.data(), key);
|
|
}
|
|
|
|
fn data(self: RequestStore) !*jetzig.data.Data {
|
|
const arena_data = try self.allocator.create(jetzig.data.Data);
|
|
arena_data.* = jetzig.data.Data.init(self.allocator);
|
|
return arena_data;
|
|
}
|
|
};
|
|
|
|
pub fn init(
|
|
allocator: std.mem.Allocator,
|
|
server: *jetzig.http.Server,
|
|
start_time: i128,
|
|
httpz_request: *httpz.Request,
|
|
httpz_response: *httpz.Response,
|
|
response: *jetzig.http.Response,
|
|
repo: *jetzig.database.Repo,
|
|
) !Request {
|
|
const path = jetzig.http.Path.init(httpz_request.url.raw);
|
|
|
|
// We can fake the HTTP method by appending `/_PATCH` (e.g.) to the end of the URL.
|
|
// This allows using PATCH, PUT, DELETE from HTML forms.
|
|
const method = path.method orelse switch (httpz_request.method) {
|
|
.DELETE => Method.DELETE,
|
|
.GET => Method.GET,
|
|
.PATCH => Method.PATCH,
|
|
.POST => Method.POST,
|
|
.HEAD => Method.HEAD,
|
|
.PUT => Method.PUT,
|
|
.OPTIONS => Method.OPTIONS,
|
|
};
|
|
|
|
const response_data = try allocator.create(jetzig.data.Data);
|
|
response_data.* = jetzig.data.Data.init(allocator);
|
|
|
|
return .{
|
|
.allocator = allocator,
|
|
.path = path,
|
|
.method = method,
|
|
.headers = jetzig.http.Headers.init(allocator, httpz_request.headers),
|
|
.server = server,
|
|
.response = response,
|
|
.response_data = response_data,
|
|
.httpz_request = httpz_request,
|
|
.httpz_response = httpz_response,
|
|
.start_time = start_time,
|
|
.store = .{ .store = server.store, .allocator = allocator },
|
|
.cache = .{ .store = server.cache, .allocator = allocator },
|
|
.repo = repo,
|
|
.global = if (@hasField(jetzig.Global, "__jetzig_default"))
|
|
undefined
|
|
else
|
|
@ptrCast(@alignCast(server.global)),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Request) void {
|
|
if (self._session) |*capture| {
|
|
capture.*.deinit();
|
|
self.allocator.destroy(capture.*);
|
|
}
|
|
if (self._cookies) |*capture| {
|
|
capture.*.deinit();
|
|
self.allocator.destroy(capture.*);
|
|
}
|
|
if (self.state != .initial) self.allocator.free(self.body);
|
|
}
|
|
|
|
/// Process request, read body if present.
|
|
pub fn process(self: *Request) !void {
|
|
self.body = self.httpz_request.body() orelse "";
|
|
self.state = .processed;
|
|
}
|
|
|
|
/// Set response headers, write response payload, and finalize the response.
|
|
pub fn respond(self: *Request) !void {
|
|
if (self.state == .initial) unreachable;
|
|
|
|
try self.setCookieHeaders();
|
|
|
|
const status = jetzig.http.status_codes.get(self.response.status_code);
|
|
self.httpz_response.status = try status.getCodeInt();
|
|
self.httpz_response.body = self.response.content;
|
|
self.state = .finalized;
|
|
}
|
|
|
|
/// Set the root value for response data.
|
|
/// ```zig
|
|
/// var root = request.data(.object)
|
|
/// var root = request.data(.array)
|
|
/// ```
|
|
pub fn data(self: Request, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.Value {
|
|
return try self.response_data.root(root);
|
|
}
|
|
|
|
/// Render a response. This function can only be called once per request (repeat calls will
|
|
/// trigger an error).
|
|
pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
|
|
if (self.isRendered()) self.rendered_multiple = true;
|
|
|
|
self.state = .rendered;
|
|
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
|
return self.rendered_view.?;
|
|
}
|
|
|
|
/// Render an error. This function can only be called once per request (repeat calls will
|
|
/// trigger an error).
|
|
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
|
|
if (self.isRendered()) self.rendered_multiple = true;
|
|
|
|
self.state = .failed;
|
|
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
|
|
return self.rendered_view.?;
|
|
}
|
|
|
|
pub inline fn isRendered(self: *const Request) bool {
|
|
return switch (self.state) {
|
|
.initial, .processed, .after_request, .before_response => false,
|
|
.rendered, .redirected, .failed, .finalized => true,
|
|
};
|
|
}
|
|
|
|
/// Issue a redirect to a new location.
|
|
/// ```zig
|
|
/// return request.redirect("https://www.example.com/", .moved_permanently);
|
|
/// ```
|
|
/// ```zig
|
|
/// return request.redirect("https://www.example.com/", .found);
|
|
/// ```
|
|
/// The second argument must be `moved_permanently` or `found`.
|
|
pub fn redirect(
|
|
self: *Request,
|
|
location: []const u8,
|
|
redirect_status: enum { moved_permanently, found },
|
|
) jetzig.views.View {
|
|
if (self.isRendered()) self.rendered_multiple = true;
|
|
|
|
self.state = .redirected;
|
|
|
|
const status_code = switch (redirect_status) {
|
|
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
|
|
.found => jetzig.http.status_codes.StatusCode.found,
|
|
};
|
|
|
|
self.redirect_state = .{ .location = location, .status_code = status_code };
|
|
return .{ .data = self.response_data, .status_code = status_code };
|
|
}
|
|
|
|
pub fn middleware(
|
|
self: *const Request,
|
|
comptime name: jetzig.http.middleware.Enum,
|
|
) jetzig.http.middleware.Type(name) {
|
|
inline for (jetzig.http.middleware.middlewares, 0..) |T, index| {
|
|
if (@hasDecl(T, "middleware_name") and std.mem.eql(u8, @tagName(name), T.middleware_name)) {
|
|
const middleware_data = self.middleware_data.get(index);
|
|
return @as(*jetzig.http.middleware.Type(name), @ptrCast(@alignCast(middleware_data))).*;
|
|
}
|
|
}
|
|
unreachable;
|
|
}
|
|
|
|
const RedirectState = struct {
|
|
location: []const u8,
|
|
status_code: jetzig.http.status_codes.StatusCode,
|
|
};
|
|
|
|
pub fn renderRedirect(self: *Request, state: RedirectState) !void {
|
|
self.response_data.reset();
|
|
|
|
self.response.headers.append("Location", state.location) catch |err| {
|
|
switch (err) {
|
|
error.JetzigTooManyHeaders => std.debug.print(
|
|
"Header limit reached. Unable to add redirect header.\n",
|
|
.{},
|
|
),
|
|
else => @panic("OOM"),
|
|
}
|
|
};
|
|
|
|
const view = jetzig.views.View{ .data = self.response_data, .status_code = state.status_code };
|
|
const status = jetzig.http.status_codes.get(state.status_code);
|
|
const maybe_template = jetzig.zmpl.findPrefixed("views", status.getCode());
|
|
self.rendered_view = view;
|
|
|
|
var root = try self.response_data.root(.object);
|
|
try root.put("location", self.response_data.string(state.location));
|
|
const content = switch (self.requestFormat()) {
|
|
.HTML, .UNKNOWN => if (maybe_template) |template| blk: {
|
|
try view.data.addConst("jetzig_view", view.data.string("internal"));
|
|
try view.data.addConst("jetzig_action", view.data.string(@tagName(state.status_code)));
|
|
break :blk try template.render(
|
|
self.response_data,
|
|
jetzig.TemplateContext,
|
|
.{ .request = self },
|
|
.{},
|
|
);
|
|
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
|
|
.JSON => blk: {
|
|
break :blk try std.json.stringifyAlloc(
|
|
self.allocator,
|
|
.{ .location = state.location, .status = .{
|
|
.message = status.getMessage(),
|
|
.code = status.getCode(),
|
|
} },
|
|
.{},
|
|
);
|
|
},
|
|
};
|
|
|
|
self.setResponse(.{ .view = view, .content = content }, .{});
|
|
}
|
|
|
|
/// Infer the current format (JSON or HTML) from the request in this order:
|
|
/// * Extension (path ends in `.json` or `.html`)
|
|
/// * `Accept` header (`application/json` or `text/html`)
|
|
/// * `Content-Type` header (`application/json` or `text/html`)
|
|
/// * Fall back to default: HTML
|
|
pub fn requestFormat(self: *const Request) jetzig.http.Request.Format {
|
|
return self.extensionFormat() orelse
|
|
self.acceptHeaderFormat() orelse
|
|
self.contentTypeHeaderFormat() orelse
|
|
.UNKNOWN;
|
|
}
|
|
|
|
/// Set the layout for the current request/response. Use this to override a `pub const layout`
|
|
/// declaration in a view, either in middleware or in a view function itself.
|
|
pub fn setLayout(self: *Request, layout: ?[]const u8) void {
|
|
if (layout) |layout_name| {
|
|
self.layout = layout_name;
|
|
self.layout_disabled = false;
|
|
} else {
|
|
self.layout_disabled = true;
|
|
}
|
|
}
|
|
|
|
/// Derive a layout name from the current request if defined, otherwise from the route (if
|
|
/// defined).
|
|
pub fn getLayout(self: *Request, route: jetzig.views.Route) ?[]const u8 {
|
|
if (self.layout_disabled) return null;
|
|
if (self.layout) |capture| return capture;
|
|
if (route.layout) |capture| return capture;
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Shortcut for `request.headers.getFirstValue`. Returns the first matching value for a given
|
|
/// header name or `null` if not found. Header names are case-insensitive.
|
|
pub fn getHeader(self: *const Request, key: []const u8) ?[]const u8 {
|
|
return self.headers.getFirstValue(key);
|
|
}
|
|
|
|
/// Return a `Value` representing request parameters. Parameters are normalized, meaning that
|
|
/// both the JSON request body and query parameters are accessed via the same interface.
|
|
/// Note that query parameters are supported for JSON requests if no request body is present,
|
|
/// otherwise the parsed JSON request body will take precedence and query parameters will be
|
|
/// ignored.
|
|
pub fn params(self: *Request) !*jetzig.data.Value {
|
|
if (self.state == .initial) unreachable;
|
|
|
|
switch (self.requestFormat()) {
|
|
.JSON => {
|
|
if (self.body.len == 0) return self.queryParams();
|
|
|
|
var params_data = try self.allocator.create(jetzig.data.Data);
|
|
params_data.* = jetzig.data.Data.init(self.allocator);
|
|
params_data.fromJson(self.body) catch |err| {
|
|
switch (err) {
|
|
error.SyntaxError, error.UnexpectedEndOfInput => return error.JetzigBodyParseError,
|
|
else => return err,
|
|
}
|
|
};
|
|
return params_data.value.?;
|
|
},
|
|
.HTML, .UNKNOWN => return self.parseQuery(),
|
|
}
|
|
}
|
|
|
|
/// Expect params that match a struct defined by `T`. If any required params are missing (or
|
|
/// cannot be matched to an enum param) then `null` is returned, otherwise a new `T` is returned
|
|
/// with values populated from matched params.
|
|
/// In both cases, `request.paramsInfo()` returns a hashmap keyed by all fields specified by `T`
|
|
/// and with values of `ParamInfo`.
|
|
/// A field in `T` can be an `enum`. In this case, if the param is a string (always true for
|
|
/// query params) then it is coerced to the target enum type if possible.
|
|
pub fn expectParams(self: *Request, T: type) !?T {
|
|
return try jetzig.http.params.expectParams(self, T);
|
|
}
|
|
|
|
/// Information about request parameters after a call to `request.expectParams`. Call
|
|
/// `request.paramsInfo()` to initialize.
|
|
///
|
|
/// Use `get` to fetch information about a specific param.
|
|
///
|
|
/// ```zig
|
|
/// const params = try request.expectParams(struct { foo: []const u8 });
|
|
/// const params_info = try request.paramsInfo();
|
|
/// const foo_info = params_info.get("foo");
|
|
/// ```
|
|
pub fn paramsInfo(self: Request) !?jetzig.http.params.ParamsInfo {
|
|
const params_info = self._params_info orelse return null;
|
|
return try params_info.init(self.allocator);
|
|
}
|
|
|
|
/// Retrieve a file from a `multipart/form-data`-encoded request body, if present.
|
|
pub fn file(self: *Request, name: []const u8) !?jetzig.http.File {
|
|
_ = try self.parseQuery();
|
|
if (self.multipart) |multipart| {
|
|
return multipart.getFile(name);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Return a `*Value` representing request parameters. This function **always** returns the
|
|
/// parsed query string and never the request body.
|
|
pub fn queryParams(self: *Request) !*jetzig.data.Value {
|
|
if (self.query_params) |parsed| return parsed.data.value.?;
|
|
|
|
const params_data = try self.allocator.create(jetzig.data.Data);
|
|
params_data.* = jetzig.data.Data.init(self.allocator);
|
|
self.query_params = try self.allocator.create(jetzig.http.Query);
|
|
self.query_params.?.* = jetzig.http.Query.init(
|
|
self.allocator,
|
|
self.path.query orelse "",
|
|
params_data,
|
|
);
|
|
try self.query_params.?.parse();
|
|
return self.query_params.?.data.value.?;
|
|
}
|
|
|
|
// Parse request body as params if present, otherwise delegate to `queryParams`.
|
|
fn parseQuery(self: *Request) !*jetzig.data.Value {
|
|
if (self.body.len == 0) return try self.queryParams();
|
|
if (self.query_body) |parsed| return parsed.data.value.?;
|
|
if (self.parsed_multipart) |parsed| return parsed.value.?;
|
|
|
|
const maybe_multipart = self.httpz_request.multiFormData() catch |err| blk: {
|
|
switch (err) {
|
|
error.NotMultipartForm => break :blk null,
|
|
else => return err,
|
|
}
|
|
};
|
|
|
|
if (maybe_multipart) |multipart| {
|
|
self.multipart = jetzig.http.MultipartQuery{ .allocator = self.allocator, .key_value = multipart };
|
|
self.parsed_multipart = try self.multipart.?.params();
|
|
return self.parsed_multipart.?.value.?;
|
|
}
|
|
|
|
const params_data = try self.allocator.create(jetzig.data.Data);
|
|
params_data.* = jetzig.data.Data.init(self.allocator);
|
|
self.query_body = try self.allocator.create(jetzig.http.Query);
|
|
self.query_body.?.* = jetzig.http.Query.init(
|
|
self.allocator,
|
|
self.body,
|
|
params_data,
|
|
);
|
|
|
|
try self.query_body.?.parse();
|
|
|
|
return self.query_body.?.data.value.?;
|
|
}
|
|
|
|
/// Parse `Cookie` header into separate cookies.
|
|
pub fn cookies(self: *Request) !*jetzig.http.Cookies {
|
|
if (self._cookies) |capture| return capture;
|
|
|
|
const cookie = self.httpz_request.headers.get("cookie");
|
|
|
|
const local_cookies = try self.allocator.create(jetzig.http.Cookies);
|
|
local_cookies.* = jetzig.http.Cookies.init(
|
|
self.allocator,
|
|
cookie orelse "",
|
|
);
|
|
try local_cookies.parse();
|
|
|
|
self._cookies = local_cookies;
|
|
|
|
return local_cookies;
|
|
}
|
|
|
|
/// Parse cookies, decrypt Jetzig cookie (`jetzig.http.Session.cookie_name`) and return a mutable
|
|
/// `jetzig.http.Session`.
|
|
pub fn session(self: *Request) !*jetzig.http.Session {
|
|
if (self._session) |capture| return capture;
|
|
|
|
const local_session = try self.allocator.create(jetzig.http.Session);
|
|
local_session.* = jetzig.http.Session.init(
|
|
self.allocator,
|
|
try self.cookies(),
|
|
self.server.env.secret,
|
|
);
|
|
local_session.parse() catch |err| {
|
|
switch (err) {
|
|
error.JetzigInvalidSessionCookie => {
|
|
try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
|
|
try local_session.reset();
|
|
},
|
|
else => return err,
|
|
}
|
|
};
|
|
|
|
self._session = local_session;
|
|
return local_session;
|
|
}
|
|
|
|
/// Return the anti-CSRF token cookie value. If no cookie exist, create it.
|
|
pub fn authenticityToken(self: *Request) ![]const u8 {
|
|
var local_session = try self.session();
|
|
|
|
return local_session.getT(.string, jetzig.authenticity_token_name) orelse blk: {
|
|
const token = try jetzig.util.generateSecret(self.allocator, 32);
|
|
try local_session.put(jetzig.authenticity_token_name, token);
|
|
break :blk local_session.getT(.string, jetzig.authenticity_token_name).?;
|
|
};
|
|
}
|
|
|
|
pub fn resourceId(self: *const Request) ![]const u8 {
|
|
return self.path.resource_id;
|
|
}
|
|
|
|
/// Return the protocol used to serve the current request by detecting the `X-Forwarded-Proto`
|
|
/// header.
|
|
pub fn protocol(self: *const Request) Protocol {
|
|
return if (self.headers.get("x-forwarded-proto")) |x_forwarded_proto|
|
|
if (std.ascii.eqlIgnoreCase(x_forwarded_proto, "https")) .https else .http
|
|
else
|
|
// TODO: Extend login when we support serving HTTPS directly.
|
|
.http;
|
|
}
|
|
|
|
/// Create a new Job. Receives a job name which must resolve to `src/app/jobs/<name>.zig`
|
|
/// Call `Job.put(...)` to set job params.
|
|
/// Call `Job.background()` to run the job outside of the request/response flow.
|
|
/// e.g.:
|
|
/// ```
|
|
/// pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
|
/// var job = try request.job("foo"); // Will invoke `process()` in `src/app/jobs/foo.zig`
|
|
/// try job.put("foo", data.string("bar"));
|
|
/// try job.background(); // Job added to queue and processed by job worker.
|
|
/// return request.render(.ok);
|
|
/// }
|
|
/// ```
|
|
pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job {
|
|
const background_job = try self.allocator.create(jetzig.Job);
|
|
background_job.* = jetzig.Job.init(
|
|
self.allocator,
|
|
self.server.store,
|
|
self.server.job_queue,
|
|
self.server.cache,
|
|
self.server.logger,
|
|
self.server.job_definitions,
|
|
job_name,
|
|
);
|
|
return background_job;
|
|
}
|
|
|
|
const RequestMail = struct {
|
|
request: *Request,
|
|
mail_params: jetzig.mail.MailParams,
|
|
name: []const u8,
|
|
|
|
// Will allow scheduling when strategy is `.later` (e.g.).
|
|
const DeliveryOptions = struct {};
|
|
|
|
pub fn deliver(self: RequestMail, strategy: enum { background, now }, options: DeliveryOptions) !void {
|
|
_ = options;
|
|
var mail_job = try self.request.job("__jetzig_mail");
|
|
|
|
try mail_job.params.put("mailer_name", mail_job.data.string(self.name));
|
|
|
|
const from = if (self.mail_params.from) |from| mail_job.data.string(from) else null;
|
|
try mail_job.params.put("from", from);
|
|
|
|
var to_array = try mail_job.data.array();
|
|
if (self.mail_params.to) |capture| {
|
|
for (capture) |to| try to_array.append(mail_job.data.string(to));
|
|
}
|
|
try mail_job.params.put("to", to_array);
|
|
|
|
const subject = if (self.mail_params.subject) |subject| mail_job.data.string(subject) else null;
|
|
try mail_job.params.put("subject", subject);
|
|
|
|
const html = if (self.mail_params.html) |html| mail_job.data.string(html) else null;
|
|
try mail_job.params.put("html", html);
|
|
|
|
const text = if (self.mail_params.text) |text| mail_job.data.string(text) else null;
|
|
try mail_job.params.put("text", text);
|
|
|
|
if (self.request.response_data.value) |value| try mail_job.params.put(
|
|
"params",
|
|
if (strategy == .now) try value.clone(self.request.allocator) else value,
|
|
);
|
|
|
|
switch (strategy) {
|
|
.background => try mail_job.schedule(),
|
|
.now => try mail_job.definition.?.runFn(
|
|
self.request.allocator,
|
|
mail_job.params,
|
|
jetzig.jobs.JobEnv{
|
|
.vars = self.request.server.env.vars,
|
|
.environment = self.request.server.env.environment,
|
|
.logger = self.request.server.logger,
|
|
.routes = self.request.server.routes,
|
|
.mailers = self.request.server.mailer_definitions,
|
|
.jobs = self.request.server.job_definitions,
|
|
.store = self.request.server.store,
|
|
.cache = self.request.server.cache,
|
|
.mutex = undefined,
|
|
},
|
|
),
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Create a new email from the mailer named `name` (`app/mailers/<name>.zig`). Pass delivery
|
|
/// params to override defaults defined my mailer (`to`, `from`, `subject`, etc.).
|
|
/// Must call `deliver` on the returned `RequestMail` to send the email.
|
|
/// Example:
|
|
/// ```zig
|
|
/// const mail = request.mail("welcome", .{ .to = &.{"hello@jetzig.dev"} });
|
|
/// try mail.deliver(.background, .{});
|
|
/// ```
|
|
pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail {
|
|
return .{
|
|
.request = self,
|
|
.name = name,
|
|
.mail_params = mail_params,
|
|
};
|
|
}
|
|
|
|
fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format {
|
|
const extension = self.path.extension orelse return null;
|
|
if (std.mem.eql(u8, extension, ".html")) {
|
|
return .HTML;
|
|
} else if (std.mem.eql(u8, extension, ".json")) {
|
|
return .JSON;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
|
if (self.httpz_request.headers.get("accept")) |value| {
|
|
if (std.mem.eql(u8, value, "text/html")) return .HTML;
|
|
if (std.mem.eql(u8, value, "application/json")) return .JSON;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format {
|
|
if (self.httpz_request.headers.get("content-type")) |value| {
|
|
if (std.mem.eql(u8, value, "text/html")) return .HTML;
|
|
if (std.mem.eql(u8, value, "application/json")) return .JSON;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 {
|
|
if (!colorized) return @tagName(self.method);
|
|
|
|
return switch (self.method) {
|
|
.GET => jetzig.colors.cyan("GET"),
|
|
.PUT => jetzig.colors.yellow("PUT"),
|
|
.PATCH => jetzig.colors.yellow("PATCH"),
|
|
.HEAD => jetzig.colors.white("HEAD"),
|
|
.POST => jetzig.colors.green("POST"),
|
|
.DELETE => jetzig.colors.red("DELETE"),
|
|
inline else => |method| jetzig.colors.white(@tagName(method)),
|
|
};
|
|
}
|
|
|
|
/// Format a status code appropriately for the current request format.
|
|
/// e.g. `.HTML` => `404 Not Found`
|
|
/// `.JSON` => `{ "message": "Not Found", "status": "404" }`
|
|
pub fn formatStatus(self: *const Request, status_code: jetzig.http.StatusCode) ![]const u8 {
|
|
const status = jetzig.http.status_codes.get(status_code);
|
|
|
|
return switch (self.requestFormat()) {
|
|
.JSON => try std.json.stringifyAlloc(self.allocator, .{
|
|
.status = .{
|
|
.message = status.getMessage(),
|
|
.code = status.getCode(),
|
|
},
|
|
}, .{}),
|
|
.HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }),
|
|
};
|
|
}
|
|
|
|
/// Override default template name for a matched route.
|
|
pub fn setTemplate(self: *Request, name: []const u8) void {
|
|
self.dynamic_assigned_template = name;
|
|
}
|
|
|
|
/// Set a custom template and render the response.
|
|
/// ```zig
|
|
/// return request.renderTemplate("blogs/comments/get", .ok);
|
|
/// ```
|
|
pub fn renderTemplate(
|
|
self: *Request,
|
|
name: []const u8,
|
|
status_code: jetzig.http.StatusCode,
|
|
) jetzig.views.View {
|
|
self.dynamic_assigned_template = name;
|
|
return self.render(status_code);
|
|
}
|
|
|
|
pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
|
|
const fields = std.meta.fields(@TypeOf(args));
|
|
var buf: [fields.len][]const u8 = undefined;
|
|
inline for (fields, 0..) |field, index| {
|
|
buf[index] = switch (@typeInfo(field.type)) {
|
|
.pointer => |info| switch (@typeInfo(info.child)) {
|
|
.@"struct", .@"union" => if (@hasDecl(info.child, "toString"))
|
|
try args[index].toString()
|
|
else
|
|
@compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."),
|
|
else => args[index], // Assume []const u8, let Zig do the work.
|
|
},
|
|
.int, .float => try std.fmt.allocPrint(self.allocator, "{d}", .{args[index]}),
|
|
else => @compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."),
|
|
};
|
|
}
|
|
return try std.mem.join(self.allocator, "/", buf[0..]);
|
|
}
|
|
|
|
pub fn joinPaths(self: *const Request, paths: []const []const []const u8) ![]const u8 {
|
|
var buf = std.ArrayList([]const u8).init(self.allocator);
|
|
defer buf.deinit();
|
|
|
|
for (paths) |subpaths| {
|
|
for (subpaths) |path| try buf.append(path);
|
|
}
|
|
return try std.mem.join(self.allocator, "/", buf.items);
|
|
}
|
|
|
|
pub fn setResponse(
|
|
self: *Request,
|
|
rendered_view: jetzig.http.Server.RenderedView,
|
|
options: struct { content_type: ?[]const u8 = null },
|
|
) void {
|
|
self.response.content = rendered_view.content;
|
|
self.response.status_code = rendered_view.view.status_code;
|
|
self.response.content_type = options.content_type orelse switch (self.requestFormat()) {
|
|
.HTML, .UNKNOWN => "text/html",
|
|
.JSON => "application/json",
|
|
};
|
|
}
|
|
|
|
fn setCookieHeaders(self: *Request) !void {
|
|
const local_cookies = self._cookies orelse return;
|
|
if (!local_cookies.modified) return;
|
|
|
|
var buf: [4096]u8 = undefined;
|
|
var header_it = local_cookies.headerIterator(&buf);
|
|
while (try header_it.next()) |header| try self.response.headers.append("Set-Cookie", header);
|
|
}
|
|
|
|
// Determine if a given route matches the current request.
|
|
pub fn match(self: *Request, route: jetzig.views.Route) !bool {
|
|
return switch (self.method) {
|
|
.GET => switch (route.action) {
|
|
.index => self.isMatch(.exact, route),
|
|
.get => self.isMatch(.resource_id, route),
|
|
.new => self.isMatch(.exact, route),
|
|
.edit => self.isMatch(.exact, route),
|
|
else => false,
|
|
},
|
|
.POST => switch (route.action) {
|
|
.post => self.isMatch(.exact, route),
|
|
else => false,
|
|
},
|
|
.PUT => switch (route.action) {
|
|
.put => self.isMatch(.resource_id, route),
|
|
else => false,
|
|
},
|
|
.PATCH => switch (route.action) {
|
|
.patch => self.isMatch(.resource_id, route),
|
|
else => false,
|
|
},
|
|
.DELETE => switch (route.action) {
|
|
.delete => self.isMatch(.resource_id, route),
|
|
else => false,
|
|
},
|
|
.HEAD, .CONNECT, .OPTIONS, .TRACE => false,
|
|
};
|
|
}
|
|
|
|
fn isMatch(
|
|
self: *Request,
|
|
match_type: enum { exact, resource_id },
|
|
route: jetzig.views.Route,
|
|
) bool {
|
|
const path = switch (match_type) {
|
|
.exact => self.path.base_path,
|
|
.resource_id => self.path.directory,
|
|
};
|
|
|
|
if (route.action == .get) {
|
|
// Special case for `/foobar/1/new` -> render `new()` - prevent matching `get`
|
|
if (std.mem.eql(u8, self.path.resource_id, "new")) return false;
|
|
// Special case for `/foobar/1/edit` -> render `edit()` - prevent matching `get`
|
|
if (std.mem.eql(u8, self.path.resource_id, "edit")) return false;
|
|
}
|
|
|
|
if (route.action == .edit and std.mem.endsWith(u8, self.path.path, "/edit")) {
|
|
var buf: [2048]u8 = undefined;
|
|
const action_path = self.path.actionPath(&buf);
|
|
if (std.mem.eql(u8, action_path, route.uri_path)) return true;
|
|
}
|
|
|
|
return std.mem.eql(u8, path, route.uri_path);
|
|
}
|