feat: extend main with parseChannelList and runRemotePublish
This commit is contained in:
1 parent
51bad5d86e
commit
29b2750464
1 file changed
+173
+173
@@ -2733,6 +2733,179 @@ fn runRemoteAggregate(allocator: std.mem.Allocator, stdout: anytype, stderr: any
|
|||||||
const specs = parseAggSpecs(allocator, rest[2]) catch return fail(stderr, "invalid aggregate spec (expected count | count:field | sum:field | avg:field | min:field | max:field | distinct:field, comma-separated)");
|
const specs = parseAggSpecs(allocator, rest[2]) catch return fail(stderr, "invalid aggregate spec (expected count | count:field | sum:field | avg:field | min:field | max:field | distinct:field, comma-separated)");
|
||||||
defer allocator.free(specs);
|
defer allocator.free(specs);
|
||||||
|
|
||||||
|
const authFlags = extractAuthFlags(allocator, rest[3..]) catch return fail(stderr, "invalid --user/--pass/--token value");
|
||||||
|
defer allocator.free(authFlags.rest);
|
||||||
|
|
||||||
|
try authenticateIfRequested(&client, stderr, authFlags.auth);
|
||||||
|
|
||||||
|
const flags = extractPageFlags(allocator, authFlags.rest) catch return fail(stderr, "invalid --limit/--offset value");
|
||||||
|
defer allocator.free(flags.rest);
|
||||||
|
|
||||||
|
const split = splitAggFilterArgs(flags.rest);
|
||||||
|
|
||||||
|
const exprText = try std.mem.join(allocator, " ", split.whereArgs);
|
||||||
|
defer allocator.free(exprText);
|
||||||
|
|
||||||
|
const havingText = if (split.havingArgs) |ha| try std.mem.join(allocator, " ", ha) else try allocator.dupe(u8, "");
|
||||||
|
defer allocator.free(havingText);
|
||||||
|
if (split.havingArgs != null and havingText.len == 0) return fail(stderr, "having requires an expression");
|
||||||
|
|
||||||
|
var orderBy: []cobalt.query.OrderBy = &.{};
|
||||||
|
defer allocator.free(orderBy);
|
||||||
|
if (split.orderArgs) |orderArgs| {
|
||||||
|
if (orderArgs.len != 1) return fail(stderr, "order requires exactly one comma-separated field list");
|
||||||
|
orderBy = parseOrderByList(allocator, orderArgs[0]) catch return fail(stderr, "invalid order field list");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, groupField, "*")) {
|
||||||
|
const values = client.tableAggregateGlobal(allocator, table, specs, havingText, exprText) catch |err| {
|
||||||
|
try stderr.print("error: {s}\n", .{@errorName(err)});
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
defer cobalt.protocol.freeAggValues(allocator, values);
|
||||||
|
// `values.len == 0` means the server's HAVING excluded the only
|
||||||
|
// global result (see `Client.tableAggregateGlobal`'s doc) - print
|
||||||
|
// nothing rather than indexing `specs` against a shorter `values`,
|
||||||
|
// which a naive `for (specs, values, ...)` would panic on.
|
||||||
|
if (values.len > 0) {
|
||||||
|
for (specs, values, 0..) |spec, v, i| {
|
||||||
|
if (i > 0) try stdout.writeAll("\t");
|
||||||
|
try printAggLabel(stdout, spec);
|
||||||
|
try stdout.writeAll("=");
|
||||||
|
try printValue(stdout, v);
|
||||||
|
}
|
||||||
|
try stdout.writeAll("\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const groups = client.tableAggregateGroups(allocator, table, groupField, specs, flags.page.offset, flags.page.limit, orderBy, havingText, exprText) catch |err| {
|
||||||
|
try stderr.print("error: {s}\n", .{@errorName(err)});
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
defer cobalt.protocol.freeAggGroupRows(allocator, groups);
|
||||||
|
for (groups) |g| {
|
||||||
|
try stdout.writeAll("group=");
|
||||||
|
try printValue(stdout, g.group);
|
||||||
|
for (specs, g.values) |spec, v| {
|
||||||
|
try stdout.writeAll("\t");
|
||||||
|
try printAggLabel(stdout, spec);
|
||||||
|
try stdout.writeAll("=");
|
||||||
|
try printValue(stdout, v);
|
||||||
|
}
|
||||||
|
try stdout.writeAll("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a remote `cobalt serve` instance and runs `table window`
|
||||||
|
/// there instead of against a local Db - the client-side counterpart of
|
||||||
|
/// `server.zig`'s `table_window` opcode, mirroring `runRemoteAggregate`'s
|
||||||
|
/// shape. Same comma-separated spec-list grammar as local `table window`
|
||||||
|
/// (`parseWindowSpecs`), same `partition <field>[,field...]`/`order
|
||||||
|
/// <field[:desc]>[,field...]` clause split (`splitWindowArgs`), and the same
|
||||||
|
/// per-row output format as the local command (row fields, then
|
||||||
|
/// `<label>=<value>`). Unlike `runRemoteAggregate`, there is no `*`
|
||||||
|
/// sentinel or global/grouped branch - a window annotation never collapses
|
||||||
|
/// rows, so there is exactly one request/response shape
|
||||||
|
/// (`Client.tableWindow`). `--limit`/`--offset` window which rows come back
|
||||||
|
/// (server-side, in `window.orderedIndices`' partition-then-order display
|
||||||
|
/// sequence, not raw scan order - see `Client.tableWindow`'s doc).
|
||||||
|
/// `--user`/`--pass`/`--token` authenticate first, needed only against a
|
||||||
|
/// server started with `serve --auth`.
|
||||||
|
/// Splits `text` on commas into channel names for `remote-subscribe` - no
|
||||||
|
/// identifier validation, unlike `parsePartitionList`, since a pub/sub
|
||||||
|
/// channel is an arbitrary string (`pubsub.zig`'s `Hub` keys purely on
|
||||||
|
/// byte equality), not a table field name.
|
||||||
|
fn parseChannelList(allocator: std.mem.Allocator, text: []const u8) ![][]const u8 {
|
||||||
|
var list = std.ArrayList([]const u8).init(allocator);
|
||||||
|
errdefer list.deinit();
|
||||||
|
var it = std.mem.splitScalar(u8, text, ',');
|
||||||
|
while (it.next()) |ch| {
|
||||||
|
if (ch.len == 0) return error.EmptyChannelName;
|
||||||
|
try list.append(ch);
|
||||||
|
}
|
||||||
|
return list.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a running `cobalt serve` and delivers a message to every
|
||||||
|
/// connection currently subscribed to `<channel>` - the client-side
|
||||||
|
/// counterpart of `server.zig`'s `publish` opcode. `<message...>` is every
|
||||||
|
/// remaining non-flag argument joined with single spaces, the same
|
||||||
|
/// "trailing free text" convention `runRemoteWindow`'s `exprText` uses, so a
|
||||||
|
/// multi-word message does not need manual quoting tricks beyond the
|
||||||
|
/// shell's own.
|
||||||
|
fn runRemotePublish(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
|
||||||
|
if (rest.len < 2) return fail(stderr, "remote-publish needs <channel> <message...> [--user NAME --pass PASS] [--token TOKEN]");
|
||||||
|
const address = try parseHostPort(stdout, addressSpec);
|
||||||
|
|
||||||
|
var client = cobalt.Client.connect(allocator, address) catch |err| {
|
||||||
|
try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
defer client.close();
|
||||||
|
|
||||||
|
const channel = rest[0];
|
||||||
|
|
||||||
|
const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
|
||||||
|
defer allocator.free(authFlags.rest);
|
||||||
|
|
||||||
|
try authenticateIfRequested(&client, stderr, authFlags.auth);
|
||||||
|
|
||||||
|
if (authFlags.rest.len == 0) return fail(stderr, "remote-publish needs a message");
|
||||||
|
const message = try std.mem.join(allocator, " ", authFlags.rest);
|
||||||
|
defer allocator.free(message);
|
||||||
|
|
||||||
|
const count = client.publish(channel, message) catch |err| {
|
||||||
|
try stderr.print("error: {s}\n", .{@errorName(err)});
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
try stdout.print("{d}\n", .{count});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a running `cobalt serve`, subscribes to every channel in the
|
||||||
|
/// comma-separated `<channel>[,channel...]` list, and blocks printing
|
||||||
|
/// `<channel>: <message>` for each message that arrives until the
|
||||||
|
/// connection ends - the client-side counterpart of `server.zig`'s
|
||||||
|
/// `subscribe` opcode and `Client.nextMessage`'s doc comment on why this
|
||||||
|
/// connection never sends another request afterward. Prints "subscribed as
|
||||||
|
/// <id>" first (the id `remote-unsubscribe`, run from a different terminal
|
||||||
|
/// against a different connection, needs to ever name this subscription
|
||||||
|
/// again) - closing this process (or the connection) still ends the whole
|
||||||
|
/// subscription outright, same as before `remote-unsubscribe` existed.
|
||||||
|
fn runRemoteSubscribe(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, addressSpec: []const u8, rest: []const []const u8) !void {
|
||||||
|
if (rest.len < 1) return fail(stderr, "remote-subscribe needs <channel>[,channel...] [--user NAME --pass PASS] [--token TOKEN]");
|
||||||
|
const address = try parseHostPort(stdout, addressSpec);
|
||||||
|
|
||||||
|
var client = cobalt.Client.connect(allocator, address) catch |err| {
|
||||||
|
try stderr.print("error: could not connect to {s}: {s}\n", .{ addressSpec, @errorName(err) });
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
defer client.close();
|
||||||
|
|
||||||
|
const channels = parseChannelList(allocator, rest[0]) catch return fail(stderr, "invalid channel list (empty channel name)");
|
||||||
|
defer allocator.free(channels);
|
||||||
|
|
||||||
|
const authFlags = extractAuthFlags(allocator, rest[1..]) catch return fail(stderr, "invalid --user/--pass/--token value");
|
||||||
|
defer allocator.free(authFlags.rest);
|
||||||
|
|
||||||
|
try authenticateIfRequested(&client, stderr, authFlags.auth);
|
||||||
|
|
||||||
|
const subscriberId = client.subscribe(channels) catch |err| {
|
||||||
|
try stderr.print("error: {s}\n", .{@errorName(err)});
|
||||||
|
std.process.exit(1);
|
||||||
|
};
|
||||||
|
try stdout.print("subscribed as {d}\n", .{subscriberId});
|
||||||
|
|
||||||
|
while (try client.nextMessage(allocator)) |msg| {
|
||||||
|
defer msg.deinit(allocator);
|
||||||
|
try stdout.print("{s}: {s}\n", .{ msg.channel, msg.payload });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a running `cobalt serve` and drops the listed channels from
|
||||||
|
/// an already-subscribed connection's set, identified by the
|
||||||
|
/// `<subscriber-id>` its `remote-subscribe` printed - the client-side
|
||||||
|
/// counterpart of `server.zig`'s `unsubscribe` opcode. Unlike
|
||||||
|
/// `remote-subscribe`, this connection is an ordinary request/response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {
|
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {
|
||||||
|
|||||||
Reference in new issue
Block a user