mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
Params helpers
Implement `request.expectParams()` to coerce params to a given struct. `request.paramsInfo()` provides information about each param (present, blank, failed + original values and errors where applicable).
This commit is contained in:
parent
e3ab49fa5a
commit
b95506caf9
@ -7,8 +7,8 @@
|
||||
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
|
||||
},
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/7f2817df78404b8a46c637c212ec1a27a66306fa.tar.gz",
|
||||
.hash = "12203a2ef05a4c3a76c1436e96c0a0aa5fc8e8406d56e50b1e9c94c394225c113b0e",
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/7b7452bc7fdb0bd2f8a6a9c4e9312900b486aeba.tar.gz",
|
||||
.hash = "1220ed127f38fa51df53a85b3cc2030a7555e34058db7fd374ebaef817abb43d35f7",
|
||||
},
|
||||
.jetkv = .{
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
|
||||
|
@ -116,8 +116,7 @@ fn parseAction(arg: []const u8) ?Action {
|
||||
fn writeAction(allocator: std.mem.Allocator, writer: anytype, action: Action) !void {
|
||||
const function = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
\\pub fn {s}({s}request: *jetzig.{s}, data: *jetzig.Data) !jetzig.View {{
|
||||
\\ _ = data;{s}
|
||||
\\pub fn {s}({s}request: *jetzig.{s}) !jetzig.View {{
|
||||
\\ return request.render({s});
|
||||
\\}}
|
||||
\\
|
||||
|
@ -2,13 +2,79 @@ const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||
const Params = struct {
|
||||
// Required param - `expectParams` returns `null` if not present:
|
||||
name: []const u8,
|
||||
// Enum params are converted from string, `expectParams` returns `null` if no match:
|
||||
favorite_animal: enum { cat, dog, raccoon },
|
||||
// Optional params are not required. Numbers are coerced from strings. `expectParams`
|
||||
// returns `null` if a type coercion fails.
|
||||
age: ?u8 = 100,
|
||||
};
|
||||
const params = try request.expectParams(Params) orelse {
|
||||
// Inspect information about the failed params with `request.paramsInfo()`:
|
||||
// std.debug.print("{?}\n", .{try request.paramsInfo()});
|
||||
return request.fail(.unprocessable_entity);
|
||||
};
|
||||
|
||||
var root = try request.data(.object);
|
||||
try root.put("info", params);
|
||||
|
||||
return request.render(.created);
|
||||
}
|
||||
|
||||
test "post" {
|
||||
test "post query params" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.POST, "/params", .{});
|
||||
try response.expectStatus(.created);
|
||||
const response1 = try app.request(.POST, "/params", .{
|
||||
.params = .{
|
||||
.name = "Bob",
|
||||
.favorite_animal = "raccoon",
|
||||
},
|
||||
});
|
||||
try response1.expectStatus(.created);
|
||||
|
||||
const response2 = try app.request(.POST, "/params", .{
|
||||
.params = .{
|
||||
.name = "Bob",
|
||||
.favorite_animal = "platypus",
|
||||
},
|
||||
});
|
||||
try response2.expectStatus(.unprocessable_entity);
|
||||
}
|
||||
|
||||
test "post json" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response1 = try app.request(.POST, "/params", .{
|
||||
.json = .{
|
||||
.name = "Bob",
|
||||
.favorite_animal = "raccoon",
|
||||
},
|
||||
});
|
||||
try response1.expectJson("$.info.name", "Bob");
|
||||
try response1.expectJson("$.info.favorite_animal", "raccoon");
|
||||
try response1.expectJson("$.info.age", 100);
|
||||
|
||||
const response2 = try app.request(.POST, "/params", .{
|
||||
.json = .{
|
||||
.name = "Hercules",
|
||||
.favorite_animal = "cat",
|
||||
.age = 11,
|
||||
},
|
||||
});
|
||||
try response2.expectJson("$.info.name", "Hercules");
|
||||
try response2.expectJson("$.info.favorite_animal", "cat");
|
||||
try response2.expectJson("$.info.age", 11);
|
||||
|
||||
const response3 = try app.request(.POST, "/params", .{
|
||||
.json = .{
|
||||
.name = "Hercules",
|
||||
.favorite_animal = "platypus",
|
||||
.age = 11,
|
||||
},
|
||||
});
|
||||
try response3.expectStatus(.unprocessable_entity);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
|
||||
pub const codes = .{
|
||||
@ -108,6 +109,8 @@ pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, is_colori
|
||||
}
|
||||
|
||||
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
|
||||
if (comptime jetzig.environment == .production) return message;
|
||||
|
||||
return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ pub const status_codes = @import("http/status_codes.zig");
|
||||
pub const StatusCode = status_codes.StatusCode;
|
||||
pub const middleware = @import("http/middleware.zig");
|
||||
pub const mime = @import("http/mime.zig");
|
||||
pub const params = @import("http/params.zig");
|
||||
|
||||
pub const SimplifiedRequest = struct {
|
||||
location: ?[]const u8,
|
||||
|
@ -23,6 +23,7 @@ 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,
|
||||
@ -172,6 +173,15 @@ pub fn respond(self: *Request) !void {
|
||||
self.httpz_response.body = self.response.content;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@ -330,24 +340,45 @@ pub fn params(self: *Request) !*jetzig.data.Value {
|
||||
.JSON => {
|
||||
if (self.body.len == 0) return self.queryParams();
|
||||
|
||||
var data = try self.allocator.create(jetzig.data.Data);
|
||||
data.* = jetzig.data.Data.init(self.allocator);
|
||||
data.fromJson(self.body) catch |err| {
|
||||
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 data.value.?;
|
||||
return params_data.value.?;
|
||||
},
|
||||
.HTML, .UNKNOWN => return self.parseQuery(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// pub fn expectParams(self: Request, T: type) ?T {
|
||||
//
|
||||
// }
|
||||
/// 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 {
|
||||
@ -364,13 +395,13 @@ pub fn file(self: *Request, name: []const u8) !?jetzig.http.File {
|
||||
pub fn queryParams(self: *Request) !*jetzig.data.Value {
|
||||
if (self.query_params) |parsed| return parsed.data.value.?;
|
||||
|
||||
const data = try self.allocator.create(jetzig.data.Data);
|
||||
data.* = jetzig.data.Data.init(self.allocator);
|
||||
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 "",
|
||||
data,
|
||||
params_data,
|
||||
);
|
||||
try self.query_params.?.parse();
|
||||
return self.query_params.?.data.value.?;
|
||||
@ -395,13 +426,13 @@ fn parseQuery(self: *Request) !*jetzig.data.Value {
|
||||
return self.parsed_multipart.?.value.?;
|
||||
}
|
||||
|
||||
const data = try self.allocator.create(jetzig.data.Data);
|
||||
data.* = jetzig.data.Data.init(self.allocator);
|
||||
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,
|
||||
data,
|
||||
params_data,
|
||||
);
|
||||
|
||||
try self.query_body.?.parse();
|
||||
|
161
src/jetzig/http/params.zig
Normal file
161
src/jetzig/http/params.zig
Normal file
@ -0,0 +1,161 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
/// See `Request.expectParams`.
|
||||
pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
|
||||
const actual_params = try request.params();
|
||||
|
||||
var t: T = undefined;
|
||||
|
||||
const fields = std.meta.fields(T);
|
||||
var statuses: [fields.len]ParamInfo = undefined;
|
||||
var failed = false;
|
||||
|
||||
inline for (fields, 0..) |field, index| {
|
||||
if (actual_params.get(field.name)) |value| {
|
||||
switch (@typeInfo(field.type)) {
|
||||
.optional => |info| if (value.coerce(info.child)) |coerced| {
|
||||
@field(t, field.name) = coerced;
|
||||
statuses[index] = .{ .present = value.* };
|
||||
} else |err| {
|
||||
failed = true;
|
||||
statuses[index] = .{ .failed = .{ .err = err, .value = value.* } };
|
||||
},
|
||||
// coerce value to target type, null on coerce error (e.g. numeric expected)
|
||||
else => {
|
||||
if (value.coerce(field.type)) |coerced| {
|
||||
@field(t, field.name) = coerced;
|
||||
statuses[index] = .{ .present = value.* };
|
||||
} else |err| {
|
||||
failed = true;
|
||||
statuses[index] = .{ .failed = .{ .err = err, .value = value.* } };
|
||||
}
|
||||
},
|
||||
}
|
||||
} else if (@typeInfo(field.type) == .optional) {
|
||||
// if no matching param found and params struct provides a default value, use it,
|
||||
// otherwise set value to null
|
||||
@field(t, field.name) = if (field.default_value) |default_value|
|
||||
@as(*field.type, @ptrCast(@alignCast(@constCast(default_value)))).*
|
||||
else
|
||||
null;
|
||||
statuses[index] = .blank;
|
||||
// We don't set `failed = true` here because optional values are not required.
|
||||
} else {
|
||||
statuses[index] = .blank;
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
request._params_info = .{
|
||||
.fields = try std.BoundedArray([]const u8, 1024).init(@intCast(fields.len)),
|
||||
.params = try std.BoundedArray(ParamInfo, 1024).init(@intCast(fields.len)),
|
||||
.required = try std.BoundedArray(bool, 1024).init(@intCast(fields.len)),
|
||||
};
|
||||
inline for (fields, 0..) |field, index| {
|
||||
request._params_info.?.fields.set(index, field.name);
|
||||
request._params_info.?.params.set(index, statuses[index]);
|
||||
request._params_info.?.required.set(index, @typeInfo(field.type) != .optional);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
/// See `Request.paramsInfo`.
|
||||
pub const ParamsInfo = struct {
|
||||
params: std.BoundedArray(ParamInfo, 1024),
|
||||
fields: std.BoundedArray([]const u8, 1024),
|
||||
required: std.BoundedArray(bool, 1024),
|
||||
state: enum { initial, ready } = .initial,
|
||||
hashmap: std.StringHashMap(ParamInfo) = undefined,
|
||||
|
||||
pub fn init(self: ParamsInfo, allocator: std.mem.Allocator) !ParamsInfo {
|
||||
var hashmap = std.StringHashMap(ParamInfo).init(allocator);
|
||||
try hashmap.ensureTotalCapacity(@intCast(self.params.len));
|
||||
for (self.fields.constSlice(), self.params.constSlice()) |field, param_info| {
|
||||
hashmap.putAssumeCapacity(field, param_info);
|
||||
}
|
||||
return .{
|
||||
.params = self.params,
|
||||
.fields = self.fields,
|
||||
.required = self.required,
|
||||
.hashmap = hashmap,
|
||||
.state = .ready,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get a information about a param. Provides param status (present/blank/failed) and
|
||||
/// original values. See `ParamInfo`.
|
||||
pub fn get(self: ParamsInfo, key: []const u8) ?ParamInfo {
|
||||
std.debug.assert(self.state == .ready);
|
||||
return self.hashmap.get(key);
|
||||
}
|
||||
|
||||
/// Detect if any required params are blank or if any errors occurred when coercing params to
|
||||
/// their target type.
|
||||
pub fn isValid(self: ParamsInfo) bool {
|
||||
for (self.params.constSlice(), self.required.constSlice()) |param, required| {
|
||||
if (required and param == .blank) return false;
|
||||
if (param == .failed) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn format(self: ParamsInfo, _: anytype, _: anytype, writer: anytype) !void {
|
||||
std.debug.assert(self.state == .ready);
|
||||
var it = self.hashmap.iterator();
|
||||
try writer.print("{s}{{ ", .{
|
||||
if (self.isValid())
|
||||
jetzig.colors.green(@typeName(@TypeOf(self)))
|
||||
else
|
||||
jetzig.colors.red(@typeName(@TypeOf(self))),
|
||||
});
|
||||
while (it.next()) |entry| {
|
||||
try writer.print("[" ++ jetzig.colors.blue("{s}") ++ ":{}] ", .{ entry.key_ptr.*, entry.value_ptr.* });
|
||||
}
|
||||
try writer.writeByte('}');
|
||||
}
|
||||
};
|
||||
|
||||
/// Status of a param as defined by the last call to `expectParams`.
|
||||
pub const ParamInfo = union(enum) {
|
||||
/// The field was matched and coerced correctly. `value` is the original param value.
|
||||
present: jetzig.Data.Value,
|
||||
/// The field was not present (regardless of whether the field was optional)
|
||||
blank: void,
|
||||
/// The field was present but could not be coerced to the required type
|
||||
failed: ParamError,
|
||||
|
||||
/// `err` is the error triggered by the type coercion attempt, `value` is the original param
|
||||
/// value.
|
||||
pub const ParamError = struct {
|
||||
err: anyerror,
|
||||
value: jetzig.Data.Value,
|
||||
|
||||
pub fn format(self: ParamError, _: anytype, _: anytype, writer: anytype) !void {
|
||||
try writer.print(
|
||||
jetzig.colors.red("{s}") ++ ":\"" ++ jetzig.colors.yellow("{}") ++ "\"",
|
||||
.{ @errorName(self.err), self.value },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn format(self: ParamInfo, _: anytype, _: anytype, writer: anytype) !void {
|
||||
switch (self) {
|
||||
.present => |present| try writer.print(
|
||||
jetzig.colors.green("present") ++ ":\"" ++ jetzig.colors.cyan("{}") ++ "\"",
|
||||
.{present},
|
||||
),
|
||||
.blank => try writer.writeAll(jetzig.colors.yellow("blank")),
|
||||
.failed => |failed| try writer.print(
|
||||
jetzig.colors.red("failed") ++ ":" ++ jetzig.colors.cyan("{}") ++ "",
|
||||
.{failed},
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
@ -151,7 +151,7 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
|
||||
|
||||
const json_banner = "\n{s}";
|
||||
|
||||
if (data.ref(std.mem.trimLeft(u8, expected_path, &.{'.'}))) |value| {
|
||||
if (data.ref(expected_path)) |value| {
|
||||
switch (value.*) {
|
||||
.string => |string| switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.pointer, .array => {
|
||||
|
@ -345,7 +345,16 @@ fn buildOptions(allocator: std.mem.Allocator, app: *const App, args: anytype) !R
|
||||
if (std.mem.eql(u8, field.name, "body")) continue;
|
||||
}
|
||||
|
||||
@compileError("Unrecognized request option: " ++ field.name);
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Unrecognized request option `{s}`. Expected: {{ {s}, {s}, {s}, {s} }}",
|
||||
.{
|
||||
jetzig.colors.yellow(field.name),
|
||||
jetzig.colors.cyan("headers"),
|
||||
jetzig.colors.cyan("json"),
|
||||
jetzig.colors.cyan("params"),
|
||||
jetzig.colors.cyan("body"),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
return .{
|
||||
|
@ -34,7 +34,7 @@ pub const StaticViewWithArgs = *const fn (
|
||||
// `Array.append(.array)`, `Array.append(.object)`, `Object.put(key, .array)`, and
|
||||
// `Object.put(key, .object)` also remove the need to use `data.array()` and `data.object()`.
|
||||
// The only remaining use is `data.root(.object)` and `data.root(.array)` which we can move to
|
||||
// `request.responseData(.object)` and `request.responseData(.array)`.
|
||||
// `request.data(.object)` and `request.data(.array)`.
|
||||
pub const LegacyViewWithoutId = *const fn (
|
||||
*jetzig.http.Request,
|
||||
*jetzig.data.Data,
|
||||
|
Loading…
x
Reference in New Issue
Block a user