mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Merge pull request #73 from jetzig-framework/custom-routes
Implement custom routes
This commit is contained in:
commit
2ddeccdadb
7
demo/src/app/views/custom/foo.zig
Normal file
7
demo/src/app/views/custom/foo.zig
Normal file
@ -0,0 +1,7 @@
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub fn bar(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
try root.put("id", data.string(id));
|
||||
return request.render(.ok);
|
||||
}
|
3
demo/src/app/views/custom/foo/bar.zmpl
Normal file
3
demo/src/app/views/custom/foo/bar.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
{{jetzig_view}}
|
||||
{{jetzig_action}}
|
||||
{{.id}}
|
@ -169,5 +169,8 @@ pub fn main() !void {
|
||||
const app = try jetzig.init(allocator);
|
||||
defer app.deinit();
|
||||
|
||||
// Example custom route:
|
||||
// app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
|
||||
|
||||
try app.start(routes, .{});
|
||||
}
|
||||
|
@ -191,5 +191,6 @@ pub fn init(allocator: std.mem.Allocator) !App {
|
||||
return .{
|
||||
.environment = environment,
|
||||
.allocator = allocator,
|
||||
.custom_routes = std.ArrayList(views.Route).init(allocator),
|
||||
};
|
||||
}
|
||||
|
@ -9,9 +9,10 @@ const App = @This();
|
||||
|
||||
environment: jetzig.Environment,
|
||||
allocator: std.mem.Allocator,
|
||||
custom_routes: std.ArrayList(jetzig.views.Route),
|
||||
|
||||
pub fn deinit(self: App) void {
|
||||
_ = self;
|
||||
pub fn deinit(self: *const App) void {
|
||||
@constCast(self).custom_routes.deinit();
|
||||
}
|
||||
|
||||
// Not used yet, but allows us to add new options to `start()` without breaking
|
||||
@ -31,30 +32,32 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
var routes = std.ArrayList(*jetzig.views.Route).init(self.allocator);
|
||||
|
||||
for (routes_module.routes) |const_route| {
|
||||
var route = try self.allocator.create(jetzig.views.Route);
|
||||
route.* = .{
|
||||
var var_route = try self.allocator.create(jetzig.views.Route);
|
||||
var_route.* = .{
|
||||
.name = const_route.name,
|
||||
.action = const_route.action,
|
||||
.view_name = const_route.view_name,
|
||||
.uri_path = const_route.uri_path,
|
||||
.view = const_route.view,
|
||||
.static_view = const_route.static_view,
|
||||
.static = const_route.static,
|
||||
.render = const_route.render,
|
||||
.renderStatic = const_route.renderStatic,
|
||||
.layout = const_route.layout,
|
||||
.template = const_route.template,
|
||||
.json_params = const_route.json_params,
|
||||
};
|
||||
|
||||
try route.initParams(self.allocator);
|
||||
try routes.append(route);
|
||||
try var_route.initParams(self.allocator);
|
||||
try routes.append(var_route);
|
||||
}
|
||||
|
||||
defer routes.deinit();
|
||||
defer for (routes.items) |route| {
|
||||
route.deinitParams();
|
||||
self.allocator.destroy(route);
|
||||
defer for (routes.items) |var_route| {
|
||||
var_route.deinitParams();
|
||||
self.allocator.destroy(var_route);
|
||||
};
|
||||
|
||||
defer for (self.custom_routes.items) |custom_route| {
|
||||
self.allocator.free(custom_route.view_name);
|
||||
self.allocator.free(custom_route.template);
|
||||
};
|
||||
|
||||
var store = try jetzig.kv.Store.init(
|
||||
@ -105,6 +108,7 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
self.allocator,
|
||||
server_options,
|
||||
routes.items,
|
||||
self.custom_routes.items,
|
||||
&routes_module.jobs,
|
||||
&routes_module.mailers,
|
||||
&mime_map,
|
||||
@ -151,3 +155,41 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn route(
|
||||
self: *const App,
|
||||
comptime method: jetzig.http.Request.Method,
|
||||
comptime path: []const u8,
|
||||
comptime module: type,
|
||||
comptime action: std.meta.DeclEnum(module),
|
||||
) void {
|
||||
const member = @tagName(action);
|
||||
const viewFn = @field(module, member);
|
||||
const module_name = comptime std.mem.trimLeft(u8, @typeName(module), "app.views.");
|
||||
|
||||
var template: [module_name.len + 1 + member.len]u8 = undefined;
|
||||
@memcpy(&template, module_name ++ "/" ++ member);
|
||||
std.mem.replaceScalar(u8, &template, '.', '/');
|
||||
|
||||
var view_name: [module_name.len]u8 = undefined;
|
||||
@memcpy(&view_name, module_name);
|
||||
std.mem.replaceScalar(u8, &view_name, '.', '/');
|
||||
|
||||
@constCast(self).custom_routes.append(.{
|
||||
.name = member,
|
||||
.action = .custom,
|
||||
.method = method,
|
||||
.view_name = self.allocator.dupe(u8, &view_name) catch @panic("OOM"),
|
||||
.uri_path = path,
|
||||
.view = comptime switch (isIncludeId(path)) {
|
||||
true => .{ .custom = .{ .with_id = viewFn } },
|
||||
false => .{ .custom = .{ .without_id = viewFn } },
|
||||
},
|
||||
.template = self.allocator.dupe(u8, &template) catch @panic("OOM"),
|
||||
.json_params = &.{},
|
||||
}) catch @panic("OOM");
|
||||
}
|
||||
|
||||
inline fn isIncludeId(comptime path: []const u8) bool {
|
||||
return std.mem.containsAtLeast(u8, path, 1, "/:");
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
/// * Extension (".json", ".html", etc.)
|
||||
/// * Query (everything after first "?" character)
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
path: []const u8,
|
||||
base_path: []const u8,
|
||||
@ -38,6 +39,21 @@ pub fn deinit(self: *Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// For a given route with a possible `:id` placeholder, return the matching URL segment for that
|
||||
/// placeholder. e.g. route with path `/foo/:id/bar` and request path `/foo/1234/bar` returns
|
||||
/// `"1234"`.
|
||||
pub fn resourceId(self: Self, route: jetzig.views.Route) []const u8 {
|
||||
var route_uri_path_it = std.mem.splitScalar(u8, route.uri_path, '/');
|
||||
var base_path_it = std.mem.splitScalar(u8, self.base_path, '/');
|
||||
|
||||
while (route_uri_path_it.next()) |route_uri_path_segment| {
|
||||
const base_path_segment = base_path_it.next() orelse return self.resource_id;
|
||||
if (std.mem.startsWith(u8, route_uri_path_segment, ":")) return base_path_segment;
|
||||
}
|
||||
|
||||
return self.resource_id;
|
||||
}
|
||||
|
||||
// Extract `"/foo/bar/baz"` from:
|
||||
// * `"/foo/bar/baz"`
|
||||
// * `"/foo/bar/baz.html"`
|
||||
|
@ -241,7 +241,7 @@ pub fn setLayout(self: *Request, layout: ?[]const u8) void {
|
||||
|
||||
/// 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 {
|
||||
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;
|
||||
|
@ -20,6 +20,7 @@ allocator: std.mem.Allocator,
|
||||
logger: jetzig.loggers.Logger,
|
||||
options: ServerOptions,
|
||||
routes: []*jetzig.views.Route,
|
||||
custom_routes: []jetzig.views.Route,
|
||||
job_definitions: []const jetzig.JobDefinition,
|
||||
mailer_definitions: []const jetzig.MailerDefinition,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
@ -35,6 +36,7 @@ pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
options: ServerOptions,
|
||||
routes: []*jetzig.views.Route,
|
||||
custom_routes: []jetzig.views.Route,
|
||||
job_definitions: []const jetzig.JobDefinition,
|
||||
mailer_definitions: []const jetzig.MailerDefinition,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
@ -47,6 +49,7 @@ pub fn init(
|
||||
.logger = options.logger,
|
||||
.options = options,
|
||||
.routes = routes,
|
||||
.custom_routes = custom_routes,
|
||||
.job_definitions = job_definitions,
|
||||
.mailer_definitions = mailer_definitions,
|
||||
.mime_map = mime_map,
|
||||
@ -163,7 +166,7 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
const route = try self.matchRoute(request, false);
|
||||
const route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
|
||||
|
||||
switch (request.requestFormat()) {
|
||||
.HTML => try self.renderHTML(request, route),
|
||||
@ -182,7 +185,7 @@ fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
|
||||
fn renderHTML(
|
||||
self: *Server,
|
||||
request: *jetzig.http.Request,
|
||||
route: ?*jetzig.views.Route,
|
||||
route: ?jetzig.views.Route,
|
||||
) !void {
|
||||
if (route) |matched_route| {
|
||||
if (zmpl.findPrefixed("views", matched_route.template)) |template| {
|
||||
@ -217,7 +220,7 @@ fn renderHTML(
|
||||
fn renderJSON(
|
||||
self: *Server,
|
||||
request: *jetzig.http.Request,
|
||||
route: ?*jetzig.views.Route,
|
||||
route: ?jetzig.views.Route,
|
||||
) !void {
|
||||
if (route) |matched_route| {
|
||||
var rendered = try self.renderView(matched_route, request, null);
|
||||
@ -254,14 +257,14 @@ pub const RenderedView = struct { view: jetzig.views.View, content: []const u8 }
|
||||
|
||||
fn renderView(
|
||||
self: *Server,
|
||||
route: *jetzig.views.Route,
|
||||
route: jetzig.views.Route,
|
||||
request: *jetzig.http.Request,
|
||||
template: ?zmpl.Template,
|
||||
) !RenderedView {
|
||||
// View functions return a `View` to encourage users to return from a view function with
|
||||
// `return request.render(.ok)`, but the actual rendered view is stored in
|
||||
// `request.rendered_view`.
|
||||
_ = route.render(route.*, request) catch |err| {
|
||||
_ = route.render(route, request) catch |err| {
|
||||
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
|
||||
if (isUnhandledError(err)) return err;
|
||||
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
||||
@ -299,7 +302,7 @@ fn renderTemplateWithLayout(
|
||||
request: *jetzig.http.Request,
|
||||
template: zmpl.Template,
|
||||
view: jetzig.views.View,
|
||||
route: *jetzig.views.Route,
|
||||
route: jetzig.views.Route,
|
||||
) ![]const u8 {
|
||||
try addTemplateConstants(view, route);
|
||||
|
||||
@ -321,9 +324,15 @@ fn renderTemplateWithLayout(
|
||||
} else return try template.render(view.data);
|
||||
}
|
||||
|
||||
fn addTemplateConstants(view: jetzig.views.View, route: *const jetzig.views.Route) !void {
|
||||
fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
|
||||
try view.data.addConst("jetzig_view", view.data.string(route.view_name));
|
||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action)));
|
||||
|
||||
const action = switch (route.action) {
|
||||
.custom => route.name,
|
||||
else => |tag| @tagName(tag),
|
||||
};
|
||||
|
||||
try view.data.addConst("jetzig_action", view.data.string(action));
|
||||
}
|
||||
|
||||
fn isBadRequest(err: anyerror) bool {
|
||||
@ -421,7 +430,7 @@ fn renderErrorView(
|
||||
switch (request.requestFormat()) {
|
||||
.HTML, .UNKNOWN => {
|
||||
if (zmpl.findPrefixed("views", route.template)) |template| {
|
||||
try addTemplateConstants(view, route);
|
||||
try addTemplateConstants(view, route.*);
|
||||
return .{ .view = view, .content = try template.render(request.response_data) };
|
||||
}
|
||||
},
|
||||
@ -490,14 +499,22 @@ fn logStackTrace(
|
||||
try self.logger.ERROR("{s}\n", .{buf.items});
|
||||
}
|
||||
|
||||
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?*jetzig.views.Route {
|
||||
fn matchCustomRoute(self: Server, request: *const jetzig.http.Request) ?jetzig.views.Route {
|
||||
for (self.custom_routes) |custom_route| {
|
||||
if (custom_route.match(request)) return custom_route;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
|
||||
for (self.routes) |route| {
|
||||
// .index routes always take precedence.
|
||||
if (route.static == static and route.action == .index and try request.match(route.*)) return route;
|
||||
if (route.static == static and route.action == .index and try request.match(route.*)) return route.*;
|
||||
}
|
||||
|
||||
for (self.routes) |route| {
|
||||
if (route.static == static and try request.match(route.*)) return route;
|
||||
if (route.static == static and try request.match(route.*)) return route.*;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -574,7 +591,7 @@ fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8
|
||||
const matched_route = try self.matchRoute(request, true);
|
||||
|
||||
if (matched_route) |route| {
|
||||
const static_path = try staticPath(request, route.*);
|
||||
const static_path = try staticPath(request, route);
|
||||
|
||||
if (static_path) |capture| {
|
||||
return static_dir.readFileAlloc(
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub const Route = @import("views/Route.zig");
|
||||
pub const View = @import("views/View.zig");
|
||||
pub const CustomRoute = @import("views/CustomRoute.zig");
|
||||
|
8
src/jetzig/views/CustomRoute.zig
Normal file
8
src/jetzig/views/CustomRoute.zig
Normal file
@ -0,0 +1,8 @@
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
method: jetzig.http.Request.Method,
|
||||
path: []const u8,
|
||||
view: union(enum) {
|
||||
with_id: *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View,
|
||||
without_id: *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View,
|
||||
},
|
@ -4,12 +4,12 @@ const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const Route = @This();
|
||||
|
||||
pub const Action = enum { index, get, post, put, patch, delete };
|
||||
pub const Action = enum { index, get, post, put, patch, delete, custom };
|
||||
pub const RenderFn = *const fn (Route, *jetzig.http.Request) anyerror!jetzig.views.View;
|
||||
pub const RenderStaticFn = *const fn (Route, *jetzig.http.StaticRequest) anyerror!jetzig.views.View;
|
||||
|
||||
const ViewWithoutId = *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
const ViewWithId = *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
pub const ViewWithoutId = *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
pub const ViewWithId = *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
const StaticViewWithoutId = *const fn (*jetzig.http.StaticRequest, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
const StaticViewWithId = *const fn (id: []const u8, *jetzig.http.StaticRequest, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
|
||||
@ -20,6 +20,7 @@ pub const DynamicViewType = union(Action) {
|
||||
put: ViewWithId,
|
||||
patch: ViewWithId,
|
||||
delete: ViewWithId,
|
||||
custom: CustomViewType,
|
||||
};
|
||||
|
||||
pub const StaticViewType = union(Action) {
|
||||
@ -29,23 +30,30 @@ pub const StaticViewType = union(Action) {
|
||||
put: StaticViewWithId,
|
||||
patch: StaticViewWithId,
|
||||
delete: StaticViewWithId,
|
||||
custom: void,
|
||||
};
|
||||
|
||||
pub const CustomViewType = union(enum) {
|
||||
with_id: ViewWithId,
|
||||
without_id: ViewWithoutId,
|
||||
};
|
||||
|
||||
pub const ViewType = union(enum) {
|
||||
static: StaticViewType,
|
||||
dynamic: DynamicViewType,
|
||||
custom: CustomViewType,
|
||||
};
|
||||
|
||||
name: []const u8,
|
||||
action: Action,
|
||||
method: jetzig.http.Request.Method = undefined, // Used by custom routes only
|
||||
view_name: []const u8,
|
||||
uri_path: []const u8,
|
||||
view: ?ViewType = null,
|
||||
static_view: ?StaticViewType = null,
|
||||
static: bool,
|
||||
view: ViewType,
|
||||
render: RenderFn = renderFn,
|
||||
renderStatic: RenderStaticFn = renderStaticFn,
|
||||
layout: ?[]const u8,
|
||||
static: bool = false,
|
||||
layout: ?[]const u8 = null,
|
||||
template: []const u8,
|
||||
json_params: []const []const u8,
|
||||
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
||||
@ -70,34 +78,56 @@ pub fn deinitParams(self: *const Route) void {
|
||||
self.params.deinit();
|
||||
}
|
||||
|
||||
/// Match a **custom** route to a request - not used by auto-generated route matching.
|
||||
pub fn match(self: Route, request: *const jetzig.http.Request) bool {
|
||||
if (self.method != request.method) return false;
|
||||
|
||||
var request_path_it = std.mem.splitScalar(u8, request.path.base_path, '/');
|
||||
var uri_path_it = std.mem.splitScalar(u8, self.uri_path, '/');
|
||||
|
||||
while (uri_path_it.next()) |expected_segment| {
|
||||
const actual_segment = request_path_it.next() orelse return false;
|
||||
if (std.mem.startsWith(u8, expected_segment, ":")) continue;
|
||||
if (!std.mem.eql(u8, expected_segment, actual_segment)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn renderFn(self: Route, request: *jetzig.http.Request) anyerror!jetzig.views.View {
|
||||
switch (self.view.?) {
|
||||
switch (self.view) {
|
||||
.dynamic => {},
|
||||
.custom => |view_type| switch (view_type) {
|
||||
.with_id => |view| return try view(request.path.resourceId(self), request, request.response_data),
|
||||
.without_id => |view| return try view(request, request.response_data),
|
||||
},
|
||||
// We only end up here if a static route is defined but its output is not found in the
|
||||
// file system (e.g. if it was manually deleted after build). This should be avoidable by
|
||||
// including the content as an artifact in the compiled executable (TODO):
|
||||
.static => return error.JetzigMissingStaticContent,
|
||||
}
|
||||
|
||||
switch (self.view.?.dynamic) {
|
||||
switch (self.view.dynamic) {
|
||||
.index => |view| return try view(request, request.response_data),
|
||||
.get => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.post => |view| return try view(request, request.response_data),
|
||||
.patch => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.put => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.delete => |view| return try view(request.path.resource_id, request, request.response_data),
|
||||
.custom => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn renderStaticFn(self: Route, request: *jetzig.http.StaticRequest) anyerror!jetzig.views.View {
|
||||
request.response_data.* = jetzig.data.Data.init(request.allocator);
|
||||
|
||||
switch (self.view.?.static) {
|
||||
switch (self.view.static) {
|
||||
.index => |view| return try view(request, request.response_data),
|
||||
.get => |view| return try view(try request.resourceId(), request, request.response_data),
|
||||
.post => |view| return try view(request, request.response_data),
|
||||
.patch => |view| return try view(try request.resourceId(), request, request.response_data),
|
||||
.put => |view| return try view(try request.resourceId(), request, request.response_data),
|
||||
.delete => |view| return try view(try request.resourceId(), request, request.response_data),
|
||||
.custom => unreachable,
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user