mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
Provide interface for adding response headers
This commit is contained in:
parent
a07c71e725
commit
be85c13369
@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
_Jetzig_ is a web framework written in [Zig](https://ziglang.org) :lizard:.
|
||||
_Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :lizard: for _Linux_, _OS X_, _Windows_, and any _OS_ that can compile _Zig_ code.
|
||||
|
||||
The framework is under active development and is currently in an alpha state.
|
||||
|
||||
@ -16,6 +16,7 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
|
||||
* [http.zig](https://github.com/karlseguin/http.zig)
|
||||
* [zig-router](https://github.com/Cloudef/zig-router)
|
||||
* [zig-webui](https://github.com/webui-dev/zig-webui/)
|
||||
* [ZTS](https://github.com/zigster64/zts)
|
||||
|
||||
## Checklist
|
||||
|
||||
@ -28,7 +29,7 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
|
||||
* :white_check_mark: Cookies.
|
||||
* :white_check_mark: Error handling.
|
||||
* :white_check_mark: Static content from /public directory.
|
||||
* :white_check_mark: Headers (available but not yet wrapped).
|
||||
* :white_check_mark: Request/response headers.
|
||||
* :white_check_mark: Stack trace output on error.
|
||||
* :white_check_mark: Static content generation.
|
||||
* :x: Param/JSON payload parsing/abstracting.
|
||||
|
@ -1,37 +1,10 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const static_params = .{
|
||||
.index = .{
|
||||
.{ .params = .{ .foo = "hi", .bar = "bye" } },
|
||||
.{ .params = .{ .foo = "hello", .bar = "goodbye" } },
|
||||
},
|
||||
.get = .{
|
||||
.{ .id = "1", .params = .{ .foo = "hi", .bar = "bye" } },
|
||||
.{ .id = "2", .params = .{ .foo = "hello", .bar = "goodbye" } },
|
||||
},
|
||||
};
|
||||
|
||||
pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
try root.put("message", data.string("Welcome to Jetzig!"));
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
if (params.get("foo")) |foo| try root.put("foo", foo);
|
||||
try request.response.headers.append("x-example-header", "example header value");
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
if (std.mem.eql(u8, id, "1")) {
|
||||
try root.put("id", data.string("id is '1'"));
|
||||
}
|
||||
|
||||
if (params.get("foo")) |foo| try root.put("foo", foo);
|
||||
|
||||
return request.render(.created);
|
||||
}
|
||||
|
@ -14,8 +14,6 @@
|
||||
<h1 class="text-3xl text-center p-3 pb-6 font-bold">{.message}</h1>
|
||||
</div>
|
||||
|
||||
<div>{.foo}</div>
|
||||
|
||||
<button hx-get="/quotes/random" hx-trigger="click" hx-target="#quote" class="bg-[#39b54a] text-white font-bold py-2 px-4 rounded">Click Me</button>
|
||||
|
||||
<div id="quote" class="p-7 mx-auto w-1/2">
|
||||
|
68
demo/src/app/views/static.zig
Normal file
68
demo/src/app/views/static.zig
Normal file
@ -0,0 +1,68 @@
|
||||
/// This example demonstrates static site generation (SSG).
|
||||
///
|
||||
/// Any view function that receives `*jetzig.StaticRequest` is considered as a SSG view, which
|
||||
/// will be invoked at build time and its content (both JSON and HTML) rendered to `static/` in
|
||||
/// the root project directory.
|
||||
///
|
||||
/// Define `pub const static_params` as a struct with fields named after each view function, with
|
||||
/// the value for each field being an array of structs with fields `params` and, where
|
||||
/// applicable (i.e. `get`, `put`, `patch`, and `delete`), `id`.
|
||||
///
|
||||
/// For each item in the provided array, a separate JSON and HTML output will be generated. At
|
||||
/// run time, requests are matched to the relevant content by comparing the request params and
|
||||
/// resource ID to locate the relevant content.
|
||||
///
|
||||
/// Launch the demo app and try the following requests:
|
||||
///
|
||||
/// ```console
|
||||
/// curl -H "Accept: application/json" \
|
||||
/// --data-bin '{"foo":"hello", "bar":"goodbye"}' \
|
||||
/// --request GET \
|
||||
/// 'http://localhost:8080/static'
|
||||
/// ```
|
||||
///
|
||||
/// ```console
|
||||
/// curl 'http://localhost:8080/static.html?foo=hi&bar=bye'
|
||||
/// ```
|
||||
///
|
||||
/// ```console
|
||||
/// curl 'http://localhost:8080/static/123.html?foo=hi&bar=bye'
|
||||
/// ```
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const static_params = .{
|
||||
.index = .{
|
||||
.{ .params = .{ .foo = "hi", .bar = "bye" } },
|
||||
.{ .params = .{ .foo = "hello", .bar = "goodbye" } },
|
||||
},
|
||||
.get = .{
|
||||
.{ .id = "1", .params = .{ .foo = "hi", .bar = "bye" } },
|
||||
.{ .id = "2", .params = .{ .foo = "hello", .bar = "goodbye" } },
|
||||
},
|
||||
};
|
||||
|
||||
pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
if (params.get("foo")) |foo| try root.put("foo", foo);
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
if (std.mem.eql(u8, id, "1")) {
|
||||
try root.put("id", data.string("id is '1'"));
|
||||
}
|
||||
|
||||
if (params.get("foo")) |foo| try root.put("foo", foo);
|
||||
if (params.get("bar")) |bar| try root.put("bar", bar);
|
||||
|
||||
return request.render(.created);
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
// This file is automatically generated at build time. Manual edits will be discarded.
|
||||
// This file should _not_ be stored in version control.
|
||||
pub const templates = struct {
|
||||
pub const static_index = @import("static/.index.zmpl.compiled.zig");
|
||||
pub const static_get = @import("static/.get.zmpl.compiled.zig");
|
||||
pub const root_index = @import("root/.index.zmpl.compiled.zig");
|
||||
pub const quotes_post = @import("quotes/.post.zmpl.compiled.zig");
|
||||
pub const quotes_get = @import("quotes/.get.zmpl.compiled.zig");
|
||||
|
@ -155,6 +155,8 @@ pub fn generateRoutes(self: *Self) !void {
|
||||
|
||||
try writer.writeAll(" };\n");
|
||||
try writer.writeAll("};");
|
||||
|
||||
// std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
|
||||
}
|
||||
|
||||
fn writeRoute(self: *Self, writer: std.ArrayList(u8).Writer, route: Function) !void {
|
||||
|
@ -8,12 +8,26 @@ pub const data = @import("jetzig/data.zig");
|
||||
pub const caches = @import("jetzig/caches.zig");
|
||||
pub const views = @import("jetzig/views.zig");
|
||||
pub const colors = @import("jetzig/colors.zig");
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
pub const App = @import("jetzig/App.zig");
|
||||
|
||||
// Convenience for view function parameters.
|
||||
/// An HTTP request which is passed to (dynamic) view functions and provides access to params,
|
||||
/// headers, and functions to render a response.
|
||||
pub const Request = http.Request;
|
||||
|
||||
/// A build-time request. Provides a similar interface to a `Request` but outputs are generated
|
||||
/// when building the application and then returned immediately to the client for matching
|
||||
/// requests.
|
||||
pub const StaticRequest = http.StaticRequest;
|
||||
|
||||
/// Generic, JSON-compatible data type. Provides `Value` which in turn provides `Object`,
|
||||
/// `Array`, `String`, `Integer`, `Float`, `Boolean`, and `NullType`.
|
||||
pub const Data = data.Data;
|
||||
|
||||
/// The return value of all view functions. Call `request.render(.ok)` in a view function to
|
||||
/// generate a `View`.
|
||||
pub const View = views.View;
|
||||
|
||||
pub const config = struct {
|
||||
|
@ -14,6 +14,9 @@ pub fn deinit(self: Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// Starts an application. `routes` should be `@import("routes").routes`, a generated file
|
||||
/// automatically created at build time. `templates` should be
|
||||
/// `@import("src/app/views/zmpl.manifest.zig").templates`, created by Zmpl at compile time.
|
||||
pub fn start(self: Self, routes: []jetzig.views.Route, templates: []jetzig.TemplateFn) !void {
|
||||
var server = jetzig.http.Server.init(
|
||||
self.allocator,
|
||||
|
@ -13,14 +13,42 @@ pub fn deinit(self: *Self) void {
|
||||
self.std_headers.deinit();
|
||||
}
|
||||
|
||||
pub fn getFirstValue(self: *Self, key: []const u8) ?[]const u8 {
|
||||
return self.std_headers.getFirstValue(key);
|
||||
// Gets the first value for a given header identified by `name`.
|
||||
pub fn getFirstValue(self: *Self, name: []const u8) ?[]const u8 {
|
||||
return self.std_headers.getFirstValue(name);
|
||||
}
|
||||
|
||||
pub fn append(self: *Self, key: []const u8, value: []const u8) !void {
|
||||
try self.std_headers.append(key, value);
|
||||
/// Appends `name` and `value` to headers.
|
||||
pub fn append(self: *Self, name: []const u8, value: []const u8) !void {
|
||||
try self.std_headers.append(name, value);
|
||||
}
|
||||
|
||||
/// Returns an iterator which implements `next()` returning each name/value of the stored headers.
|
||||
pub fn iterator(self: *Self) Iterator {
|
||||
return Iterator{ .std_headers = self.std_headers };
|
||||
}
|
||||
|
||||
const Iterator = struct {
|
||||
std_headers: std.http.Headers,
|
||||
index: usize = 0,
|
||||
|
||||
const Header = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
/// Returns the next item in the current iteration of headers.
|
||||
pub fn next(self: *Iterator) ?Header {
|
||||
if (self.std_headers.list.items.len > self.index) {
|
||||
const std_header = self.std_headers.list.items[self.index];
|
||||
self.index += 1;
|
||||
return .{ .name = std_header.name, .value = std_header.value };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
const allocator = std.testing.allocator;
|
||||
var headers = std.http.Headers.init(allocator);
|
||||
|
@ -43,9 +43,22 @@ pub fn init(
|
||||
_ => return error.JetzigUnsupportedHttpMethod,
|
||||
};
|
||||
|
||||
// TODO: Replace all this with a `Path` type which exposes all components of the path in a
|
||||
// sensible way:
|
||||
// * Array of segments: "/foo/bar/baz" => .{ "foo", "bar", "baz" }
|
||||
// * Resource ID: "/foo/bar/baz/1" => "1"
|
||||
// * Extension: "/foo/bar/baz/1.json" => ".json"
|
||||
// * Query params: "/foo/bar/baz?foo=bar&baz=qux" => .{ .foo = "bar", .baz => "qux" }
|
||||
// * Anything else ?
|
||||
var it = std.mem.splitScalar(u8, response.std_response.request.target, '/');
|
||||
var segments = std.ArrayList([]const u8).init(allocator);
|
||||
while (it.next()) |segment| try segments.append(segment);
|
||||
while (it.next()) |segment| {
|
||||
if (std.mem.indexOfScalar(u8, segment, '?')) |query_index| {
|
||||
try segments.append(segment[0..query_index]);
|
||||
} else {
|
||||
try segments.append(segment);
|
||||
}
|
||||
}
|
||||
|
||||
var cookies = try allocator.create(jetzig.http.Cookies);
|
||||
cookies.* = jetzig.http.Cookies.init(
|
||||
@ -149,7 +162,7 @@ fn parseQueryString(self: *Self) !bool {
|
||||
if (self.path.len - 1 < index + 1) return false;
|
||||
|
||||
self.query.* = jetzig.http.Query.init(
|
||||
self.server.allocator,
|
||||
self.allocator,
|
||||
self.path[index + 1 ..],
|
||||
self.query_data,
|
||||
);
|
||||
@ -214,7 +227,7 @@ pub fn resourceModifier(self: *Self) ?Modifier {
|
||||
}
|
||||
|
||||
pub fn resourceName(self: *Self) []const u8 {
|
||||
if (self.segments.items.len == 0) return "default";
|
||||
if (self.segments.items.len == 0) return "default"; // Should never happen ?
|
||||
|
||||
const basename = std.fs.path.basename(self.segments.items[self.segments.items.len - 1]);
|
||||
if (std.mem.indexOfScalar(u8, basename, '?')) |index| {
|
||||
@ -264,7 +277,7 @@ fn isMatch(self: *Self, match_type: enum { exact, resource_id }, route: jetzig.v
|
||||
.resource_id => self.pathWithoutExtensionAndResourceId(),
|
||||
};
|
||||
|
||||
return (std.mem.eql(u8, path, route.uri_path));
|
||||
return std.mem.eql(u8, path, route.uri_path);
|
||||
}
|
||||
|
||||
// TODO: Be a bit more deterministic in identifying extension, e.g. deal with `.` characters
|
||||
|
@ -30,8 +30,8 @@ pub fn init(
|
||||
|
||||
pub fn deinit(self: *const Self) void {
|
||||
self.headers.deinit();
|
||||
// self.allocator.free(self.content);
|
||||
// self.allocator.free(self.content_type);
|
||||
self.allocator.destroy(self.headers);
|
||||
self.std_response.deinit();
|
||||
}
|
||||
|
||||
const ResetState = enum { reset, closing };
|
||||
@ -49,11 +49,18 @@ pub fn wait(self: *const Self) !void {
|
||||
try self.std_response.wait();
|
||||
}
|
||||
|
||||
/// Finalizes a request. Appends any stored headers, sets the response status code, and writes
|
||||
/// the response body.
|
||||
pub fn finish(self: *const Self) !void {
|
||||
self.std_response.status = switch (self.status_code) {
|
||||
inline else => |status_code| @field(std.http.Status, @tagName(status_code)),
|
||||
};
|
||||
|
||||
var it = self.headers.iterator();
|
||||
while (it.next()) |header| {
|
||||
try self.std_response.headers.append(header.name, header.value);
|
||||
}
|
||||
|
||||
try self.std_response.send();
|
||||
try self.std_response.writeAll(self.content);
|
||||
try self.std_response.finish();
|
||||
@ -76,12 +83,3 @@ pub fn setTransferEncoding(self: *const Self, transfer_encoding: TransferEncodin
|
||||
// TODO: Chunked encoding
|
||||
self.std_response.transfer_encoding = .{ .content_length = transfer_encoding.content_length };
|
||||
}
|
||||
|
||||
pub fn dupe(self: *const Self) !Self {
|
||||
return .{
|
||||
.allocator = self.allocator,
|
||||
.status_code = self.status_code,
|
||||
.content_type = try self.allocator.dupe(u8, self.content_type),
|
||||
.content = try self.allocator.dupe(u8, self.content),
|
||||
};
|
||||
}
|
||||
|
@ -67,18 +67,22 @@ pub fn listen(self: *Self) !void {
|
||||
|
||||
fn processRequests(self: *Self) !void {
|
||||
while (true) {
|
||||
var std_response = try self.server.accept(.{ .allocator = self.allocator });
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var std_response = try self.server.accept(.{ .allocator = allocator });
|
||||
|
||||
var response = try jetzig.http.Response.init(
|
||||
self.allocator,
|
||||
allocator,
|
||||
&std_response,
|
||||
);
|
||||
errdefer response.deinit();
|
||||
errdefer arena.deinit();
|
||||
|
||||
try response.headers.append("Connection", "close");
|
||||
|
||||
while (response.reset() != .closing) {
|
||||
self.processNextRequest(&response) catch |err| {
|
||||
self.processNextRequest(allocator, &response) catch |err| {
|
||||
switch (err) {
|
||||
error.EndOfStream, error.ConnectionResetByPeer => continue,
|
||||
error.UnknownHttpMethod => continue, // TODO: Render 400 Bad Request here ?
|
||||
@ -88,10 +92,11 @@ fn processRequests(self: *Self) !void {
|
||||
}
|
||||
|
||||
response.deinit();
|
||||
arena.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
fn processNextRequest(self: *Self, response: *jetzig.http.Response) !void {
|
||||
fn processNextRequest(self: *Self, allocator: std.mem.Allocator, response: *jetzig.http.Response) !void {
|
||||
try response.wait();
|
||||
|
||||
self.start_time = std.time.nanoTimestamp();
|
||||
@ -99,10 +104,7 @@ fn processNextRequest(self: *Self, response: *jetzig.http.Response) !void {
|
||||
const body = try response.read();
|
||||
defer self.allocator.free(body);
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
var request = try jetzig.http.Request.init(arena.allocator(), self, response, body);
|
||||
var request = try jetzig.http.Request.init(allocator, self, response, body);
|
||||
defer request.deinit();
|
||||
|
||||
var middleware_data = try jetzig.http.middleware.beforeMiddleware(&request);
|
||||
@ -180,16 +182,11 @@ fn renderHTML(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
response.content = "";
|
||||
response.status_code = .not_found;
|
||||
response.content_type = "text/html";
|
||||
return;
|
||||
} else {
|
||||
response.content = "";
|
||||
response.status_code = .not_found;
|
||||
response.content_type = "text/html";
|
||||
}
|
||||
|
||||
response.content = "";
|
||||
response.status_code = .not_found;
|
||||
response.content_type = "text/html";
|
||||
}
|
||||
|
||||
fn renderJSON(
|
||||
@ -329,22 +326,10 @@ fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?jetzig
|
||||
return null;
|
||||
}
|
||||
|
||||
fn matchStaticParams(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Route) !?usize {
|
||||
_ = self;
|
||||
const params = try request.params();
|
||||
|
||||
for (route.params.items, 0..) |static_params, index| {
|
||||
if (try static_params.getValue("params")) |expected_params| {
|
||||
if (expected_params.eql(params)) return index;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const StaticResource = struct { content: []const u8, mime_type: []const u8 = "application/octet-stream" };
|
||||
|
||||
fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResource {
|
||||
const public_content = try self.matchPublicContent(request);
|
||||
const public_content = try matchPublicContent(request);
|
||||
if (public_content) |content| return .{ .content = content };
|
||||
|
||||
const static_content = try self.matchStaticContent(request);
|
||||
@ -359,9 +344,7 @@ fn matchStaticResource(self: *Self, request: *jetzig.http.Request) !?StaticResou
|
||||
return null;
|
||||
}
|
||||
|
||||
fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
|
||||
_ = self;
|
||||
|
||||
fn matchPublicContent(request: *jetzig.http.Request) !?[]const u8 {
|
||||
if (request.path.len < 2) return null;
|
||||
if (request.method != .GET) return null;
|
||||
|
||||
@ -403,7 +386,8 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
|
||||
const matched_route = try self.matchRoute(request, true);
|
||||
|
||||
if (matched_route) |route| {
|
||||
const static_path = try self.staticPath(request, route);
|
||||
const static_path = try staticPath(request, route);
|
||||
|
||||
if (static_path) |capture| {
|
||||
return static_dir.readFileAlloc(
|
||||
request.allocator,
|
||||
@ -421,9 +405,10 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn staticPath(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
|
||||
_ = self;
|
||||
fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]const u8 {
|
||||
const params = try request.params();
|
||||
defer params.deinit();
|
||||
|
||||
const extension = switch (request.requestFormat()) {
|
||||
.HTML, .UNKNOWN => ".html",
|
||||
.JSON => ".json",
|
||||
@ -439,8 +424,8 @@ fn staticPath(self: *Self, request: *jetzig.http.Request, route: jetzig.views.Ro
|
||||
.string => |capture| {
|
||||
if (!std.mem.eql(u8, capture.value, request.resourceId())) continue;
|
||||
},
|
||||
// Should be unreachable but we want to avoid a runtime panic.
|
||||
inline else => continue,
|
||||
// Should be unreachable - this means generated `routes.zig` is incoherent:
|
||||
inline else => return error.JetzigRouteError,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user