'rr' (request/response)... WIP (pre-alpha)... command line REPL and scripting environment for cloud APIs

So I’ve been working on this more or less non-stop for the last couple of months. My goal for this ‘release’ was to have a reliable way to process different syntaxes, so that I could adjust the rules on the fly as I’m improving this, without causing any catastrophic breakage. I also wanted to be able to do this with a repl. from here I can easily add the ability load external scripts. I am releasing this with a lot of room for improvements. But I think my string parsing algorithm is solid. There’s a lot of things I could add at this point without too much effort (i.e. reading json configs to define the destination host). So for now I am just restricting the http request to the one hardcoded in the source…

So a little bit about what I’m trying to do. It’s basically to have a scripting language to accept variables and objects and pass them into different kindof of http requests. Sort of a way to plug into a remote API. Also being able to query and process JSON responses, which I got a pretty got start on, but here I am simply outputting a constant JSON response body.

For example, I would like to have a script like this

$obj = { hello: "okay" }
$query = 'hello.id[5]'

.post ($obj.JSON) { $query }

this would process a JavaScript style object stored in a variable, converting it to a JSON syntax and sending it as the request body over a post request, with the destination host defined in the config file. The incoming JSON response would be processed by the query variable, and output appropriately. For now it’s only supporting string variable declarations (which should validate with single or double quotes). These variables are automatically formatted into a url querystring and sent as a GET request to a minimal node server.

I was hoping to have a little big more, but unfortunately I hit a lot of dead-ends along the way, so I will be taking a break from it for a little while in order to gain more perspective. The upside is that I learned a lot about Zig, everything in std.mem, and the different kinds of lists and ways to iterate over them, when a for loop is more useful and when a while loop is useful. Also continue expressions in while loops… very useful. I ended up writing a simple custom “Forward” iterator to keep things simple. So i can just use like it.next() in the while loops without having to worry about tracking an index.

Anyway at this point I’m relieved just to be able to throw this out there. At least now I got past the hard parts (i think), so I should just be able to improve this bit by bit without too much effort.

To use the repl, you just put in string variables line by line like so…

$hello=‘okay’
$okay=“hello”

a double enter will trigger the script to process and send the http request based on the variables stored, converting them to url parameters in the process

you can also do multiline input by appending a \ at the end of each line. these lines will all be concatenated before processing.

definitely a lot of loose threads here, but it does work as is, with the provided javascript server.

source code for server:

const http = require('http');
const url = require('url')

const port = 3000;
const host = 'localhost';

const user = { hello: 'okay' };

http.createServer((req, res) => {

   console.log(url.parse(req.url).query);

   res.writeHead(200, { 'Content-Type': 'application/json' });
   res.end(JSON.stringify(user));

}).listen(port, (err) => {

   if (err) throw err;

   console.log(`${host}:${port}`);

});

and the main src…

const std = @import("std");

const Value = union {
    null: void,
    bool: bool,
    integer: i64,
    float: f64,
    string: []const u8,
    array: std.ArrayList(Value),
    object: std.StringArrayHashMap(Value),
};

const ForwardIterator = struct {
    haystack: *[]u8,
    index: usize = 0,

    fn next(self: *ForwardIterator) ?*u8 {
        if (self.index < self.haystack.*.len) {
            self.index += 1;
            return &self.haystack.*[self.index - 1];
        }
        return null;
    }
};

const State = struct {
    lines: []u8,
    query: *std.ArrayList(u8),
    value: *std.ArrayList(u8),
    vars: *std.StringArrayHashMap(Value),

    fn init(lines: []u8, query: *std.ArrayList(u8), value: *std.ArrayList(u8), vars: *std.StringArrayHashMap(Value)) State {
        return State{
            .lines = lines,
            .query = query,
            .value = value,
            .vars = vars,
        };
    }

    fn exec(self: *State) !void {
        var it = ForwardIterator{ .haystack = &self.lines };
        outer: while (it.next()) |a| {
            switch (a.*) {
                '$' => {
                    first: while (it.next()) |b| {
                        switch (b.*) {
                            '\n' => {
                                std.debug.print("INVALID::\\n\n", .{});
                                break :outer;
                            },

                            '=' => {
                                while (it.next()) |c| {
                                    switch (c.*) {
                                        '\n' => {
                                            std.debug.print("INVALID::\\n\n", .{});
                                            break :outer;
                                        },

                                        '\'' => {
                                            while (it.next()) |d| {
                                                switch (d.*) {
                                                    '\n' => {
                                                        std.debug.print("INVALID::\\n\n", .{});
                                                        break :outer;
                                                    },

                                                    '\\' => {
                                                        while (it.next()) |e| {
                                                            switch (e.*) {
                                                                '\\' => {
                                                                    try self.value.append(e.*);
                                                                    break;
                                                                },

                                                                '\'' => {
                                                                    try self.value.append(e.*);
                                                                    break;
                                                                },

                                                                else => {
                                                                    std.debug.print("INVALID::{c}\n", .{e.*});
                                                                    break :outer;
                                                                },
                                                            }
                                                        }
                                                    },

                                                    '\'' => {
                                                        while (it.next()) |e| {
                                                            switch (e.*) {
                                                                '\n' => {
                                                                    try self.vars.put(try self.query.toOwnedSlice(), Value{ .string = try self.value.toOwnedSlice() });
                                                                    break :first;
                                                                },

                                                                else => {
                                                                    std.debug.print("INVALID::{c}\n", .{e.*});
                                                                    break :outer;
                                                                },
                                                            }
                                                        }
                                                    },

                                                    else => {
                                                        try self.value.append(d.*);
                                                    },
                                                }
                                            }
                                        },

                                        '"' => {
                                            while (it.next()) |d| {
                                                switch (d.*) {
                                                    '\n' => {
                                                        std.debug.print("INVALID::\\n\n", .{});
                                                        break :outer;
                                                    },

                                                    '\\' => {
                                                        while (it.next()) |e| {
                                                            switch (e.*) {
                                                                '\\' => {
                                                                    try self.value.append(e.*);
                                                                    break;
                                                                },

                                                                '"' => {
                                                                    try self.value.append(e.*);
                                                                    break;
                                                                },

                                                                else => {
                                                                    std.debug.print("INVALID::{c}\n", .{e.*});
                                                                    break :outer;
                                                                },
                                                            }
                                                        }
                                                    },

                                                    '"' => {
                                                        while (it.next()) |e| {
                                                            switch (e.*) {
                                                                '\n' => {
                                                                    try self.vars.put(try self.query.toOwnedSlice(), Value{ .string = try self.value.toOwnedSlice() });
                                                                    break :first;
                                                                },

                                                                else => {
                                                                    std.debug.print("INVALID::{c}\n", .{e.*});
                                                                    break :outer;
                                                                },
                                                            }
                                                        }
                                                    },

                                                    else => {
                                                        try self.value.append(d.*);
                                                    },
                                                }
                                            }
                                        },

                                        else => {
                                            std.debug.print("INVALID::{c}\n", .{c.*});
                                            break :outer;
                                        },
                                    }
                                }
                            },

                            else => {
                                switch (b.*) {
                                    '_' => {
                                        try self.query.append(b.*);
                                    },

                                    else => {
                                        if (std.ascii.isAlphanumeric(b.*)) {
                                            try self.query.append(b.*);
                                        } else {
                                            std.debug.print("INVALID::{c}\n", .{b.*});
                                            break :outer;
                                        }
                                    },
                                }
                            },
                        }
                    }
                },

                else => {
                    std.debug.print("INVALID::{c}\n", .{a.*});
                    break :outer;
                },
            }
        }
    }
};

test {
    const allocator = std.testing.allocator;

    std.debug.print("\n~\n> ", .{});

    const stdin = std.io.getStdIn();
    var reader = stdin.reader();

    var input = std.ArrayList(u8).init(allocator);
    var multi = std.ArrayList(u8).init(allocator);
    var lines = std.ArrayList(u8).init(allocator);

    var query = std.ArrayList(u8).init(allocator);
    defer query.deinit();

    var value = std.ArrayList(u8).init(allocator);
    defer value.deinit();

    var vars = std.StringArrayHashMap(Value).init(allocator);
    defer vars.deinit();

    while (reader.streamUntilDelimiter(input.writer(), '\n', 1000) != error.EndOfStream) {
        if (input.items.len > 0 and input.items[input.items.len - 1] == '\r') {
            input.shrinkRetainingCapacity(input.items.len - 1);
        }

        while (input.items.len > 0 and input.items[input.items.len - 1] == '\\' and reader.streamUntilDelimiter(multi.writer(), '\n', 1000) != error.EndOfStream) {
            input.shrinkRetainingCapacity(input.items.len - 1);

            if (multi.items.len > 0 and multi.items[multi.items.len - 1] == '\r') {
                multi.shrinkRetainingCapacity(multi.items.len - 1);
            }

            try input.appendSlice(try multi.toOwnedSlice());
        }

        if (input.items.len == 0) {
            var s: State = State.init(try lines.toOwnedSlice(), &query, &value, &vars);
            try s.exec();

            var it = s.vars.iterator();

            var url: []const u8 = "http://127.0.0.1:3000/?";

            while (it.next()) |kv| {
                url = try std.fmt.allocPrint(allocator, "{s}{s}={s}&", .{ url, try std.Uri.escapeString(allocator, kv.key_ptr.*), try std.Uri.escapeString(allocator, kv.value_ptr.*.string) });
            }

            std.debug.print("{s}\n", .{url});

            const uri = std.Uri.parse(url) catch unreachable;

            var client: std.http.Client = .{ .allocator = allocator };
            defer client.deinit();

            var headers = std.http.Headers{ .allocator = allocator };
            defer headers.deinit();

            try headers.append("accept", "*/*");

            var request = try client.request(.GET, uri, headers, .{});
            defer request.deinit();

            try request.start();
            try request.wait();

            try std.testing.expect(request.response.status == .ok);

            const body = request.reader().readAllAlloc(allocator, 8192) catch unreachable;
            defer allocator.free(body);

            std.debug.print("{s}\n", .{body});

            query.clearRetainingCapacity();
            value.clearRetainingCapacity();
            vars.clearRetainingCapacity();
        } else {
            try input.append('\n');
            try lines.appendSlice(try input.toOwnedSlice());
        }

        std.debug.print("> ", .{});
    }

    std.debug.print("//EOF\n", .{});
}

not quite ready to release this on github, so i will just drop the license here as well…

ISC License

Copyright (c) 2023, Michael S. Scott (mscott9437@gmail.com)

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

A big thanks to all the Zig team, for building a language interesting enough to keep my attention. Definitely any feedback is appreciated, especially as to how I might optimize this, or if there is any easier way to do what i’m already doing.

Edit: just to clarify the official name of the project is ‘rr’ which stands for request/response, and I am planning to implement the processing of script files with the .rr extension

4 Likes

i just edited this thing. didn’t realize it was going to bump it

Honestly, I’m impressed with how far you’ve come since your first post here on the forum… it really wasn’t that long ago.

Nothing wrong with taking a break to mentally regroup - sometimes that’s where your best ideas will come from. Likewise, it helps me check my own “sunken cost” because I find it easy to convince myself to move forward when really a rewrite would better service the whole thing.

I’ll give it a good read soon and give you some feedback but I just had to compliment you on your progress. Keep up the great work!

2 Likes

Thanks. Definitely your advice along the way was indispensable. I really underestimated the significance of Zig’s static typing early on. Since I made that post I did get it pretty well in my head now how I will do the next update. I just haven’t sat down to work it out yet. I have maybe 2 or 3 other programming-related priorities in the queue, but it will be good to get back to it. Basically I will start by refactoring the scan() function to make it easier to read and organize, and I’m also looking at adding a second enum to allow for more detailed tracking of the state. From there it should be easier to add more and more functions. Also I want to give some kind of overview explaining my approach to how I came up with this initial code, to see how it holds up and also to maybe come up with some alternative ideas.

  • self.haystack.*.len

This can be reduced to self.haystack.len - you can access data members directly through the pointers.

  • multi.items[multi.items.len - 1]

Thankfully, we have multi.getLast() which you can use in many places here.

So let’s have a look at this block of code here:

const ForwardIterator = struct {
    haystack: *[]u8,
    index: usize = 0,

    fn next(self: *ForwardIterator) ?*u8 {
        if (self.index < self.haystack.*.len) {
            self.index += 1;
            return &self.haystack.*[self.index - 1];
        }
        return null;
    }
};

You don’t need this to be a *[]u8 because []u8 is already a pointer - slices are pointers with an additional length (often called “fat pointers”). You can just copy the slice and that will automatically give you something that points to the same memory.

Really, this could be:

const ForwardIterator = struct {
    haystack: []const u8,
    index: usize = 0,

    fn next(self: *ForwardIterator) ?u8 {
        if (self.index < self.haystack.len) {
            self.index += 1;
            return self.haystack[self.index - 1];
        }
        return null;
    }
};

Then, you could write switch (d) without needing the extra dereference. I changed the haystack member to []const u8 because I didn’t see where you were changing the underlying data of haystack. You may be and I just missed it (I’m just eyeballing it at this moment in time).

Regarding this bit here…

self.index += 1;
return self.haystack[self.index - 1];

I compared the assembly (because I was curious) of that approach against:

const tmp = self.haystack[self.index];
self.index += 1;
return tmp;

And even in debug, they reduce to essentially the same thing.

        push    rbp
        mov     rbp, rsp
        sub     rsp, 112
        mov     qword ptr [rbp - 56], rsi
        mov     qword ptr [rbp - 48], rdi
        mov     qword ptr [rbp - 40], rdi
        mov     qword ptr [rbp - 32], rsi
        mov     rax, qword ptr [rsi + 16]
        mov     rcx, qword ptr [rsi]
        mov     rcx, qword ptr [rsi + 8]
        cmp     rax, rcx
        jb      .LBB6_2
        jmp     .LBB6_3
        push    rbp
        mov     rbp, rsp
        sub     rsp, 96
        mov     qword ptr [rbp - 48], rsi
        mov     qword ptr [rbp - 40], rdi
        mov     qword ptr [rbp - 32], rdi
        mov     qword ptr [rbp - 24], rsi
        mov     rax, qword ptr [rsi + 16]
        mov     rcx, qword ptr [rsi]
        mov     rcx, qword ptr [rsi + 8]
        cmp     rax, rcx
        jb      .LBB3_2
        jmp     .LBB3_3

And that’s both in Debug mode - the difference becomes so small at any further optimization level that it actually becomes non-trivial to write an example where the compiler doesn’t just optimize the whole thing away… I’d need to write an actual looping example that calls it multiple times, probably.

I personally prefer the second one because it avoids the “I add one and then take it away” semantics, but if you prefer that then it’ll be the same thing essentially. It’s one less line of code, so you can make an argument there.

Anyhow, I haven’t looked at your loop logic, but those are just some basic things to consider.

2 Likes