mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
WIP
This commit is contained in:
parent
3bd296821c
commit
94f67dd4a2
@ -27,7 +27,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<button id="reset-button">Reset Game</button>
|
||||
<button jetzig-click="reset" id="reset-button">Reset Game</button>
|
||||
|
||||
<div id="victor"></div>
|
||||
|
||||
@ -70,14 +70,4 @@
|
||||
jetzig.channel.transform("cell", (value) => (
|
||||
{ player: "✈️", cpu: "🦎" }[value] || ""
|
||||
));
|
||||
document.querySelectorAll("#board div.cell").forEach(element => {
|
||||
element.addEventListener("click", () => {
|
||||
jetzig.channel.actions.move(parseInt(element.dataset.cell));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector("#reset-button").addEventListener("click", () => {
|
||||
jetzig.channel.actions.reset();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
110
src/Routes.zig
110
src/Routes.zig
@ -12,6 +12,7 @@ buffer: std.ArrayList(u8),
|
||||
dynamic_routes: std.ArrayList(Function),
|
||||
static_routes: std.ArrayList(Function),
|
||||
channel_routes: std.ArrayList([]const u8),
|
||||
channel_actions: std.StringHashMap(std.StringHashMap([]const []const u8)),
|
||||
module_paths: std.StringHashMap(void),
|
||||
data: *jetzig.data.Data,
|
||||
|
||||
@ -124,6 +125,7 @@ pub fn init(
|
||||
.static_routes = std.ArrayList(Function).init(allocator),
|
||||
.dynamic_routes = std.ArrayList(Function).init(allocator),
|
||||
.channel_routes = std.ArrayList([]const u8).init(allocator),
|
||||
.channel_actions = std.StringHashMap(std.StringHashMap([]const []const u8)).init(allocator),
|
||||
.module_paths = std.StringHashMap(void).init(allocator),
|
||||
.data = data,
|
||||
};
|
||||
@ -163,6 +165,7 @@ pub fn generateRoutes(self: *Routes) ![]const u8 {
|
||||
\\});
|
||||
\\
|
||||
);
|
||||
|
||||
try writer.writeAll(
|
||||
\\
|
||||
\\pub const mailers = [_]jetzig.MailerDefinition{
|
||||
@ -410,10 +413,35 @@ fn writeChannelRoutes(self: *Routes, writer: anytype) !void {
|
||||
defer self.allocator.free(relative_path);
|
||||
const view_name = chompExtension(relative_path);
|
||||
|
||||
var actions_buf = std.ArrayList(u8).init(self.allocator);
|
||||
const actions_writer = actions_buf.writer();
|
||||
if (self.channel_actions.get(path)) |actions| {
|
||||
var it = actions.iterator();
|
||||
while (it.next()) |entry| {
|
||||
try actions_writer.print(
|
||||
\\.{{ .name = "{s}", .params = &.{{
|
||||
, .{entry.key_ptr.*});
|
||||
for (entry.value_ptr.*, 0..) |param, index| {
|
||||
if (index == 0) continue; // Skip `self` argument.
|
||||
try actions_writer.print(
|
||||
\\.{{ .name = "{s}" }},
|
||||
, .{param});
|
||||
}
|
||||
try actions_writer.writeAll("},},");
|
||||
}
|
||||
}
|
||||
|
||||
try writer.print(
|
||||
\\.{{ "{0s}", jetzig.channels.Route.initComptime(@import("{1s}"), "{0s}") }}
|
||||
\\.{{
|
||||
\\ "{0s}",
|
||||
\\ jetzig.channels.Route.initComptime(
|
||||
\\ @import("{1s}"),
|
||||
\\ "{0s}",
|
||||
\\ &.{{{2s}}}
|
||||
\\ ),
|
||||
\\}}
|
||||
\\
|
||||
, .{ view_name, module_path });
|
||||
, .{ view_name, module_path, actions_buf.items });
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,6 +524,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
const decl_name = self.ast.tokenSlice(container_token - 2);
|
||||
if (std.mem.eql(u8, decl_name, "Channel")) {
|
||||
try channel_routes.append(path);
|
||||
try self.parseChannel(container, path);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
@ -527,6 +556,83 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
};
|
||||
}
|
||||
|
||||
// Although we mostly evaluate channel routes at comptime, we need to parse the function
|
||||
// signatures in `Actions` to get argument names (Zig only reflects the types).
|
||||
fn parseChannel(self: *Routes, channel: std.zig.Ast.full.ContainerDecl, path: []const u8) !void {
|
||||
for (channel.ast.members) |member| {
|
||||
const tag = self.ast.nodeTag(member);
|
||||
switch (tag) {
|
||||
.simple_var_decl => {
|
||||
const var_decl = self.ast.simpleVarDecl(member);
|
||||
const var_name = self.ast.tokenSlice(self.ast.nodeMainToken(member) + 1);
|
||||
if (std.mem.eql(u8, var_name, "Actions")) {
|
||||
const init_node = var_decl.ast.init_node.unwrap() orelse continue;
|
||||
switch (self.ast.nodeTag(init_node)) {
|
||||
.container_decl_two,
|
||||
.container_decl_two_trailing,
|
||||
.container_decl,
|
||||
.container_decl_trailing,
|
||||
=> |container_tag| {
|
||||
var buf: [2]std.zig.Ast.Node.Index = undefined;
|
||||
const container = switch (container_tag) {
|
||||
.container_decl_two,
|
||||
.container_decl_two_trailing,
|
||||
=> self.ast.containerDeclTwo(&buf, init_node),
|
||||
.container_decl,
|
||||
.container_decl_trailing,
|
||||
=> self.ast.containerDecl(init_node),
|
||||
else => unreachable,
|
||||
};
|
||||
const container_token = container.ast.main_token;
|
||||
const decl_name = self.ast.tokenSlice(container_token - 2);
|
||||
if (std.mem.eql(u8, decl_name, "Actions")) {
|
||||
try self.parseChannelActions(container, path);
|
||||
}
|
||||
},
|
||||
else => continue,
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parseChannelActions(self: *Routes, actions: std.zig.Ast.full.ContainerDecl, path: []const u8) !void {
|
||||
for (actions.ast.members) |member| {
|
||||
const tag = self.ast.nodes.items(.tag)[@intFromEnum(member)];
|
||||
switch (tag) {
|
||||
.fn_proto,
|
||||
.fn_proto_multi,
|
||||
.fn_proto_one,
|
||||
.fn_proto_simple,
|
||||
.fn_decl,
|
||||
=> {
|
||||
var buf: [1]std.zig.Ast.Node.Index = undefined;
|
||||
const func = self.ast.fullFnProto(&buf, member).?;
|
||||
const visib_token = func.visib_token orelse continue;
|
||||
if (!std.mem.eql(u8, self.ast.tokenSlice(visib_token), "pub")) continue;
|
||||
const func_name_token = func.name_token orelse continue;
|
||||
const func_name = self.ast.tokenSlice(func_name_token);
|
||||
|
||||
var params_buf = std.ArrayList([]const u8).init(self.allocator);
|
||||
var params_it = func.iterate(&self.ast);
|
||||
while (params_it.next()) |param| {
|
||||
try params_buf.append(
|
||||
try self.allocator.dupe(u8, self.ast.tokenSlice(param.name_token.?)),
|
||||
);
|
||||
}
|
||||
const result = try self.channel_actions.getOrPut(path);
|
||||
if (!result.found_existing) {
|
||||
result.value_ptr.* = std.StringHashMap([]const []const u8).init(self.allocator);
|
||||
}
|
||||
try result.value_ptr.put(try self.allocator.dupe(u8, func_name), try params_buf.toOwnedSlice());
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the `pub const static_params` definition and into a `jetzig.data.Value`.
|
||||
fn parseStaticParamsDecl(self: *Routes, decl: std.zig.Ast.full.VarDecl, params: *jetzig.data.Value) !void {
|
||||
const init_node = decl.ast.init_node.unwrap() orelse return;
|
||||
|
@ -148,7 +148,12 @@ fn encodeParams(Routes: type) !std.StaticStringMap([]const u8) {
|
||||
actions: []ActionSpec,
|
||||
pub const ActionSpec = struct {
|
||||
name: []const u8,
|
||||
params: []const u8,
|
||||
params: []const ParamSpec,
|
||||
|
||||
pub const ParamSpec = struct {
|
||||
type: []const u8,
|
||||
name: []const u8,
|
||||
};
|
||||
};
|
||||
};
|
||||
const Tuple = std.meta.Tuple(&.{ []const u8, []const u8 });
|
||||
@ -168,10 +173,17 @@ fn encodeParams(Routes: type) !std.StaticStringMap([]const u8) {
|
||||
switch (@typeInfo(@TypeOf(@field(view.module.Channel.Actions, decl.name)))) {
|
||||
.@"fn" => |info| {
|
||||
verifyParams(info.params, view.name, decl.name);
|
||||
const route = Routes.channel_routes.get(view.name).?;
|
||||
const action = for (route.actions) |action| {
|
||||
if (std.mem.eql(u8, action.name, decl.name)) break action;
|
||||
} else unreachable;
|
||||
if (info.params.len > 1) {
|
||||
var params: [info.params.len - 1]u8 = undefined;
|
||||
var params: [info.params.len - 1]Spec.ActionSpec.ParamSpec = undefined;
|
||||
for (info.params[1..], 0..) |param, param_index| {
|
||||
params[param_index] = jsonTypeName(param.type.?);
|
||||
params[param_index] = .{
|
||||
.type = jsonTypeName(param.type.?),
|
||||
.name = action.params[param_index].name,
|
||||
};
|
||||
}
|
||||
actions[decl_index] = .{ .name = decl.name, .params = ¶ms };
|
||||
} else {
|
||||
@ -210,13 +222,13 @@ fn verifyParams(
|
||||
if (params[0].type.? != jetzig.channels.Channel) @compileError(missing_param);
|
||||
}
|
||||
|
||||
fn jsonTypeName(T: type) u8 {
|
||||
fn jsonTypeName(T: type) []const u8 {
|
||||
return switch (T) {
|
||||
[]const u8 => 's',
|
||||
[]const u8 => "string",
|
||||
else => switch (@typeInfo(T)) {
|
||||
.float, .comptime_float => 'f',
|
||||
.bool => 'b',
|
||||
.int, .comptime_int => 'i',
|
||||
.float, .comptime_float => "float",
|
||||
.int, .comptime_int => "integer",
|
||||
.bool => "bool",
|
||||
else => @compileError("Unsupported Channel Action argument type: " ++ @typeName(T)),
|
||||
},
|
||||
};
|
||||
|
@ -5,12 +5,22 @@ const Route = @This();
|
||||
receiveMessageFn: ?*const fn (jetzig.channels.Message) anyerror!void = null,
|
||||
openConnectionFn: ?*const fn (jetzig.channels.Channel) anyerror!void = null,
|
||||
path: []const u8,
|
||||
actions: []const Action,
|
||||
|
||||
pub const Action = struct {
|
||||
name: []const u8,
|
||||
params: []const Param,
|
||||
|
||||
pub const Param = struct {
|
||||
name: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn receiveMessage(route: Route, message: jetzig.channels.Message) !void {
|
||||
if (route.receiveMessageFn) |func| try func(message);
|
||||
}
|
||||
|
||||
pub fn initComptime(T: type, path: []const u8) Route {
|
||||
pub fn initComptime(T: type, path: []const u8, actions: []const Action) Route {
|
||||
comptime {
|
||||
if (!@hasDecl(T, "Channel")) return .{};
|
||||
const openConnectionFn = if (@hasDecl(T.Channel, "open")) T.Channel.open else null;
|
||||
@ -20,6 +30,7 @@ pub fn initComptime(T: type, path: []const u8) Route {
|
||||
.openConnectionFn = openConnectionFn,
|
||||
.receiveMessageFn = receiveMessageFn,
|
||||
.path = path,
|
||||
.actions = actions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ jetzig = window.jetzig;
|
||||
jetzig.channel = {
|
||||
websocket: null,
|
||||
actions: {},
|
||||
action_specs: {},
|
||||
stateChangedCallbacks: [],
|
||||
messageCallbacks: [],
|
||||
invokeCallbacks: {},
|
||||
@ -25,7 +26,6 @@ jetzig = window.jetzig;
|
||||
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
|
||||
onMessage: function(callback) { this.messageCallbacks.push(callback); },
|
||||
init: function(host, path) {
|
||||
console.log("here");
|
||||
this.websocket = new WebSocket(`ws://${host}${path}`);
|
||||
this.websocket.addEventListener("message", (event) => {
|
||||
const state_tag = "__jetzig_channel_state__:";
|
||||
@ -51,20 +51,33 @@ jetzig = window.jetzig;
|
||||
} else if (event.data.startsWith(actions_tag)) {
|
||||
const data = JSON.parse(event.data.slice(actions_tag.length));
|
||||
data.actions.forEach(action => {
|
||||
this.actions[action.name] = {
|
||||
this.action_specs[action.name] = {
|
||||
callback: (...params) => {
|
||||
if (action.params.length != params.length) {
|
||||
throw new Error(`Invalid params for action '${action.name}'`);
|
||||
throw new Error(`Invalid params for action '${action.name}'. Expected ${action.params.length} params, found ${params.length}`);
|
||||
}
|
||||
[...action.params].forEach((param, index) => {
|
||||
const map = {
|
||||
s: "string",
|
||||
b: "boolean",
|
||||
i: "number",
|
||||
f: "number",
|
||||
};
|
||||
if (map[param] !== typeof params[index]) {
|
||||
throw new Error(`Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${map[param]}, found ${typeof params[index]}`);
|
||||
if (param.type !== typeof params[index]) {
|
||||
const err = `Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${param.type}, found ${typeof params[index]}`;
|
||||
switch (param.type) {
|
||||
case "string":
|
||||
params[index] = `${params[index]}`;
|
||||
break;
|
||||
case "integer":
|
||||
try { params[index] = parseInt(params[index]) } catch {
|
||||
throw new Error(err);
|
||||
};
|
||||
break;
|
||||
case "float":
|
||||
try { params[index] = parseFloat(params[index]) } catch {
|
||||
throw new Error(err);
|
||||
};
|
||||
case "boolean":
|
||||
params[index] = ["true", "y", "1", "yes", "t"].includes(params[index]);
|
||||
break;
|
||||
default:
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,8 +85,28 @@ jetzig = window.jetzig;
|
||||
},
|
||||
spec: { ...action },
|
||||
};
|
||||
this.actions[action.name] = this.action_specs[action.name].callback;
|
||||
});
|
||||
document.querySelectorAll('[jetzig-click]').forEach(element => {
|
||||
const ref = element.getAttribute('jetzig-click');
|
||||
const action = this.action_specs[ref];
|
||||
if (action) {
|
||||
element.addEventListener('click', () => {
|
||||
const args = [];
|
||||
action.spec.params.forEach(param => {
|
||||
const arg = element.dataset[param.name];
|
||||
if (arg === undefined) {
|
||||
throw new Error(`Expected 'data-${param.name}' attribute for '${action.name}' click handler.`);
|
||||
} else {
|
||||
args.push(element.dataset[param.name]);
|
||||
}
|
||||
});
|
||||
action.callback(...args);
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unknown click handler: '${ref}'`);
|
||||
}
|
||||
});
|
||||
console.log(this.actions);
|
||||
} else {
|
||||
const data = JSON.parse(event.data);
|
||||
this.messageCallbacks.forEach((callback) => {
|
||||
@ -108,13 +141,6 @@ jetzig = window.jetzig;
|
||||
this.elementMap[ref].push(element);
|
||||
this.transformerMap[id] = element.getAttribute('jetzig-transform');
|
||||
});
|
||||
document.querySelectorAll('[jetzig-click]').forEach(element => {
|
||||
const ref = element.getAttribute('jetzig-click');
|
||||
const action = this.actions[ref];
|
||||
if (action) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// this.websocket.addEventListener("open", (event) => {
|
||||
// // TODO
|
||||
|
Loading…
x
Reference in New Issue
Block a user