From 9186bb58ecad1cca7dbc491242586f8036bb8070 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 14 Jan 2023 16:13:14 +0100 Subject: [PATCH] endpoint example: the beginnings --- build.zig | 1 + examples/endpoint/endpoints.zig | 103 +++++++++++++++++++++++++++++ examples/endpoint/main.zig | 40 +++++++++++ examples/endpoint/users.zig | 113 ++++++++++++++++++++++++++++++++ src/endpoint.zig | 80 ++++++++++++---------- src/zap.zig | 10 +++ 6 files changed, 314 insertions(+), 33 deletions(-) create mode 100644 examples/endpoint/endpoints.zig create mode 100644 examples/endpoint/main.zig create mode 100644 examples/endpoint/users.zig diff --git a/build.zig b/build.zig index 77ec0be..e49c16f 100644 --- a/build.zig +++ b/build.zig @@ -23,6 +23,7 @@ pub fn build(b: *std.build.Builder) !void { .{ .name = "routes", .src = "examples/routes/routes.zig" }, .{ .name = "serve", .src = "examples/serve/serve.zig" }, .{ .name = "hello_json", .src = "examples/hello_json/hello_json.zig" }, + .{ .name = "endpoint", .src = "examples/endpoint/main.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; diff --git a/examples/endpoint/endpoints.zig b/examples/endpoint/endpoints.zig new file mode 100644 index 0000000..d330719 --- /dev/null +++ b/examples/endpoint/endpoints.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const zap = @import("zap"); +const Users = @import("users.zig"); + +// the Endpoints + +pub const Self = @This(); + +var alloc: std.mem.Allocator = undefined; +var endpoint: zap.SimpleEndpoint = undefined; +var list_endpoint: zap.SimpleEndpoint = undefined; +var users: Users = undefined; + +pub fn init( + a: std.mem.Allocator, + user_path: []const u8, + userlist_path: []const u8, +) void { + users = Users.init(a); + alloc = a; + endpoint = zap.SimpleEndpoint.init(.{ + .path = user_path, + .get = getUser, + .post = null, + .put = null, + .delete = null, + }); + list_endpoint = zap.SimpleEndpoint.init(.{ + .path = userlist_path, + .get = listUsers, + .post = null, + .put = null, + .delete = null, + }); +} + +pub fn getUsers() *Users { + return &users; +} + +pub fn getUserEndpoint() *zap.SimpleEndpoint { + return &endpoint; +} + +pub fn getUserListEndpoint() *zap.SimpleEndpoint { + return &list_endpoint; +} + +fn userIdFromPath(path: []const u8) ?usize { + if (path.len >= endpoint.settings.path.len + 2) { + if (path[endpoint.settings.path.len] != '/') { + std.debug.print("no slash\n", .{}); + return null; + } + const idstr = path[endpoint.settings.path.len + 1 ..]; + std.debug.print("idstr={s}\n", .{idstr}); + return std.fmt.parseUnsigned(usize, idstr, 10) catch null; + } + return null; +} + +var jsonbuf: [1024]u8 = undefined; +fn stringify(value: anytype, options: std.json.StringifyOptions) ?[]const u8 { + var fba = std.heap.FixedBufferAllocator.init(&jsonbuf); + var string = std.ArrayList(u8).init(fba.allocator()); + if (std.json.stringify(value, options, string.writer())) { + return string.items; + } else |_| { // error + return null; + } +} + +pub fn getUser(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void { + _ = e; + std.debug.print("getUser()\n", .{}); + if (r.path) |path| { + std.debug.print("getUser({s})\n", .{path}); + if (userIdFromPath(path)) |id| { + std.debug.print("getUser({})\n", .{id}); + if (users.get(id)) |user| { + std.debug.print("getUser(): {}\n", .{user}); + if (stringify(user, .{})) |json| { + _ = r.sendJson(json); + } + } + } else { + std.debug.print("User not found\n", .{}); + } + } +} + +pub fn listUsers(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void { + std.debug.print("listUsers()\n", .{}); + _ = r; + _ = e; + var l = std.ArrayList(Users.User).init(alloc); + if (users.list(&l)) {} else |_| { + return; + } + // if (stringify(l, .{})) |json| { + // _ = r.sendJson(json); + // } +} diff --git a/examples/endpoint/main.zig b/examples/endpoint/main.zig new file mode 100644 index 0000000..3653f59 --- /dev/null +++ b/examples/endpoint/main.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const zap = @import("zap"); +const Endpoints = @import("endpoints.zig"); + +pub fn main() !void { + const allocator = std.heap.page_allocator; + // setup listener + var listener = zap.SimpleEndpointListener.init( + allocator, + .{ + .port = 3000, + .on_request = null, + .log = true, + }, + ); + + Endpoints.init(allocator, "/user", "/list"); + + // add endpoints + try listener.addEndpoint(Endpoints.getUserEndpoint()); + try listener.addEndpoint(Endpoints.getUserListEndpoint()); + + // fake some users + var uid: usize = undefined; + uid = try Endpoints.getUsers().addByName("renerocksai", null); + std.debug.print("Added user {}\n", .{uid}); + uid = try Endpoints.getUsers().addByName("renerocksai", "your mom"); + std.debug.print("Added user {}\n", .{uid}); + + // listen + try listener.listen(); + + std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + + // and run + zap.start(.{ + .threads = 2, + .workers = 2, + }); +} diff --git a/examples/endpoint/users.zig b/examples/endpoint/users.zig new file mode 100644 index 0000000..788a0b5 --- /dev/null +++ b/examples/endpoint/users.zig @@ -0,0 +1,113 @@ +const std = @import("std"); + +alloc: std.mem.Allocator = undefined, +users: std.AutoHashMap(usize, InternalUser) = undefined, + +pub const Self = @This(); + +const InternalUser = struct { + id: usize = 0, + firstnamebuf: [64]u8, + firstnamelen: usize, + lastnamebuf: [64]u8, + lastnamelen: usize, +}; + +pub const User = struct { + first_name: []const u8, + last_name: []const u8, +}; + +pub fn init(a: std.mem.Allocator) Self { + return .{ + .alloc = a, + .users = std.AutoHashMap(usize, InternalUser).init(a), + }; +} + +/// the request will be freed (and reused by facilio) when it's +/// completed, so we take copies of the names +pub fn addByName(self: *Self, first: ?[]const u8, last: ?[]const u8) !usize { + var created = try self.alloc.alloc(InternalUser, 1); + var user = created[0]; + user.id = self.users.count() + 1; + user.firstnamelen = 0; + user.lastnamelen = 0; + if (first) |firstname| { + std.mem.copy(u8, user.firstnamebuf[0..], firstname); + user.firstnamelen = firstname.len; + } + if (last) |lastname| { + std.mem.copy(u8, user.lastnamebuf[0..], lastname); + user.lastnamelen = lastname.len; + } + try self.users.put(user.id, user); + return user.id; +} + +pub fn delete(self: *Self, id: usize) bool { + if (self.users.get(id)) |pUser| { + self.alloc.free(pUser); + return self.users.remove(id); + } + return false; +} + +pub fn get(self: *Self, id: usize) ?User { + if (self.users.get(id)) |pUser| { + return .{ + .first_name = pUser.firstnamebuf[0..pUser.firstnamelen], + .last_name = pUser.lastnamebuf[0..pUser.lastnamelen], + }; + } + return null; +} + +pub fn update(self: *Self, id: usize, first: ?[]const u8, last: ?[]const u8) bool { + if (self.users.get(id)) |pUser| { + pUser.firstnamelen = 0; + pUser.lastnamelen = 0; + if (first) |firstname| { + std.mem.copy(u8, pUser.firstnamebuf[0..], firstname); + pUser.firstnamelen = firstname.len; + } + if (last) |lastname| { + std.mem.copy(u8, pUser.lastname[0..], lastname); + pUser.lastnamelen = lastname.len; + } + return true; + } + return false; +} + +// populate the list +pub fn list(self: *Self, out: *std.ArrayList(User)) !void { + var it = JsonUserIterator.init(&self.users); + while (it.next()) |user| { + try out.append(user); + } +} + +const JsonUserIterator = struct { + it: std.AutoHashMap(usize, InternalUser).ValueIterator = undefined, + const This = @This(); + + // careful: + // - Self refers to the file's struct + // - This refers to the JsonUserIterator struct + pub fn init(internal_users: *std.AutoHashMap(usize, InternalUser)) This { + return .{ + .it = internal_users.valueIterator(), + }; + } + + pub fn next(this: *This) ?User { + if (this.it.next()) |pUser| { + return User{ + .first_name = pUser.firstnamebuf[0..pUser.firstnamelen], + .last_name = pUser.lastnamebuf[0..pUser.lastnamelen], + }; + } + return null; + } +}; diff --git a/src/endpoint.zig b/src/endpoint.zig index bd66808..1c94210 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -1,91 +1,105 @@ const std = @import("std"); const zap = @import("zap.zig"); const Request = zap.SimpleRequest; -const RequestFn = zap.SimpleHttpRequestFn; const ListenerSettings = zap.SimpleHttpListenerSettings; const Listener = zap.SimpleHttpListener; -const SimpleEndpointSettings = struct { +pub const RequestFn = *const fn (self: *SimpleEndpoint, r: Request) void; +pub const SimpleEndpointSettings = struct { path: []const u8, - get: ?RequestFn, - post: ?RequestFn, - put: ?RequestFn, - delete: ?RequestFn, + get: ?RequestFn = null, + post: ?RequestFn = null, + put: ?RequestFn = null, + delete: ?RequestFn = null, }; -const SimpleEndpoint = struct { +pub const SimpleEndpoint = struct { settings: SimpleEndpointSettings, - var Self = @This(); + const Self = @This(); pub fn init(s: SimpleEndpointSettings) Self { return .{ - .path = s.path, - .get = s.get orelse &nop, - .post = s.post orelse &nop, - .put = s.put orelse &nop, - .delete = s.delete orelse &nop, + .settings = .{ + .path = s.path, + .get = s.get orelse &nop, + .post = s.post orelse &nop, + .put = s.put orelse &nop, + .delete = s.delete orelse &nop, + }, }; } - fn nop(r: Request) void { + fn nop(self: *SimpleEndpoint, r: Request) void { + _ = self; _ = r; } - pub fn onRequest(r: zap.SimpleRequest) void { + pub fn onRequest(self: *SimpleEndpoint, r: zap.SimpleRequest) void { if (r.method) |m| { if (std.mem.eql(u8, m, "GET")) - // TODO - nop(); + return self.settings.get.?(self, r); + if (std.mem.eql(u8, m, "POST")) + return self.settings.post.?(self, r); + if (std.mem.eql(u8, m, "PUT")) + return self.settings.put.?(self, r); + if (std.mem.eql(u8, m, "DELETE")) + return self.settings.delete.?(self, r); } } }; -const EndpointListenerError = error{ +pub const EndpointListenerError = error{ EndpointPathShadowError, }; -var endpoints: std.StringHashMap(*SimpleEndpoint) = undefined; +// var endpoints: std.StringHashMap(*SimpleEndpoint) = undefined; +var endpoints: std.ArrayList(*SimpleEndpoint) = undefined; // NOTE: We switch on path.startsWith -> so use endpoints with distinctly // starting names!! -const SimpleEndpointListener = struct { +pub const SimpleEndpointListener = struct { listener: Listener, allocator: std.mem.Allocator, - var Self = @This(); + const Self = @This(); pub fn init(a: std.mem.Allocator, l: ListenerSettings) Self { - l.on_request = onRequest; - endpoints = std.StringHashMap(*SimpleEndpoint).init(a); + var ls = l; + ls.on_request = onRequest; + endpoints = std.ArrayList(*SimpleEndpoint).init(a); return .{ - .listener = Listener.init(l), + .listener = Listener.init(ls), .allocator = a, }; } + pub fn listen(self: *SimpleEndpointListener) !void { + try self.listener.listen(); + } + pub fn addEndpoint(self: *SimpleEndpointListener, e: *SimpleEndpoint) !void { - var it = endpoints.keyIterator(); - while (it.next()) |existing_path| { + _ = self; + for (endpoints.items) |other| { if (std.mem.startsWith( u8, - existing_path, - e.path, + other.settings.path, + e.settings.path, ) or std.mem.startsWith( u8, - e.path, - existing_path, + e.settings.path, + other.settings.path, )) { return EndpointListenerError.EndpointPathShadowError; } } - try self.endpoints.put(e.path, e); + try endpoints.append(e); } fn onRequest(r: Request) void { if (r.path) |p| { - for (endpoints) |e| { - if (std.mem.startsWith(u8, p, e.path)) { + for (endpoints.items) |e| { + if (std.mem.startsWith(u8, p, e.settings.path)) { e.onRequest(r); return; } diff --git a/src/zap.zig b/src/zap.zig index 186ff21..6b5e747 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -7,6 +7,8 @@ pub const C = @cImport({ @cInclude("fio.h"); }); +pub usingnamespace @import("endpoint.zig"); + pub fn fio2str(o: C.FIOBJ) ?[]const u8 { if (o == 0) return null; const x: C.fio_str_info_s = C.fiobj_obj2cstr(o); @@ -62,6 +64,14 @@ pub const SimpleRequest = struct { ), body.len); } + pub fn sendJson(self: *const Self, json: []const u8) c_int { + self.setContentType(.JSON); + return C.http_send_body(self.h, @intToPtr( + *anyopaque, + @ptrToInt(json.ptr), + ), json.len); + } + pub fn setContentType(self: *const Self, c: ContentType) void { self.setHeader("content-type", switch (c) { .TEXT => "text/plain",