summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEkaitz Zarraga <ekaitz@elenq.tech>2023-04-09 22:28:36 +0200
committerEkaitz Zarraga <ekaitz@elenq.tech>2023-04-09 22:28:36 +0200
commit57982c29668ffb45fb0b7a574fee34261978453a (patch)
treea06dca8e2e9252f78365afc3c153f05fef545e58
Some http random parser and stuff
-rw-r--r--build.zig34
-rw-r--r--src/http.zig392
-rw-r--r--src/main.zig39
-rw-r--r--src/weatherFetcher.zig101
4 files changed, 566 insertions, 0 deletions
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..834c43a
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,34 @@
+const std = @import("std");
+
+pub fn build(b: *std.build.Builder) void {
+ // Standard target options allows the person running `zig build` to choose
+ // what target to build for. Here we do not override the defaults, which
+ // means any target is allowed, and the default is native. Other options
+ // for restricting supported target set are available.
+ const target = b.standardTargetOptions(.{});
+
+ // Standard release options allow the person running `zig build` to select
+ // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
+ const mode = b.standardReleaseOptions();
+
+ const exe = b.addExecutable("elTiempo", "src/main.zig");
+ exe.setTarget(target);
+ exe.setBuildMode(mode);
+ exe.install();
+
+ const run_cmd = exe.run();
+ run_cmd.step.dependOn(b.getInstallStep());
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+
+ const exe_tests = b.addTest("src/main.zig");
+ exe_tests.setTarget(target);
+ exe_tests.setBuildMode(mode);
+
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&exe_tests.step);
+}
diff --git a/src/http.zig b/src/http.zig
new file mode 100644
index 0000000..49b4e71
--- /dev/null
+++ b/src/http.zig
@@ -0,0 +1,392 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const split = std.mem.split;
+const net = std.net;
+const expect = std.testing.expect;
+const expectError = std.testing.expectError;
+const StringHashMap = std.StringHashMap;
+
+pub const HttpError = error {
+ ParsingError
+};
+
+/// HttpStatus
+pub const HttpStatus = struct{
+ protocol: []const u8,
+ status: usize,
+ status_msg: []const u8,
+};
+
+pub const HttpStatusParser = struct{
+ status: HttpStatus,
+ rawline: []u8,
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) Self{
+ return Self{
+ .status = undefined, // Can I do this?
+ .rawline = "",
+ .allocator = allocator,
+ };
+ }
+ /// Takes a reader and consumes one status line of HTTP response allocating
+ /// it's own copy of the line. Must call `deinit()` to clean the
+ /// allocations.
+ /// Fills it's `status` field with the results of that.
+ pub fn parseReader(self: *Self, reader: anytype) !void{
+ self.rawline = try reader.readUntilDelimiterAlloc(self.allocator, '\r', 1000);
+ // Drop \n
+ _ = try reader.readByte();
+
+ var components = split(u8, self.rawline, " ");
+
+ const p = if(components.next()) |p| p else return error.ParsingError;
+ const s = if(components.next()) |s| s else return error.ParsingError;
+ const sm = components.rest();
+ if (sm.len == 0) return error.ParsingError;
+ self.status.protocol = p;
+ self.status.status = try std.fmt.parseUnsigned(usize, s, 10);
+ self.status.status_msg = sm;
+ }
+ pub fn deinit(self: *Self) void{
+ self.allocator.free(self.rawline);
+ }
+};
+
+
+test "200 OK is parsed correctly in an HttpStatus" {
+ const status_line = "HTTP/1.1 200 OK\r\n";
+ const allocator = std.testing.allocator;
+
+ var fis = std.io.fixedBufferStream(status_line);
+ const reader = fis.reader();
+
+ var status = HttpStatusParser.init(allocator);
+ defer status.deinit();
+ try status.parseReader(reader);
+
+ try expect( std.mem.eql(u8, status.status.protocol, "HTTP/1.1") );
+ try expect( status.status.status == 200 );
+ try expect( std.mem.eql(u8, status.status.status_msg, "OK") );
+
+}
+
+test "306 Switch Proxy is parsed correctly in an HttpStatus" {
+ const status_line = "HTTP/1.1 306 Switch Proxy\r\n";
+ const allocator = std.testing.allocator;
+
+ var fis = std.io.fixedBufferStream(status_line);
+ const reader = fis.reader();
+
+ var status = HttpStatusParser.init(allocator);
+ defer status.deinit();
+ try status.parseReader(reader);
+
+ try expect( std.mem.eql(u8, status.status.protocol, "HTTP/1.1") );
+ try expect( status.status.status == 306 );
+ try expect( std.mem.eql(u8, status.status.status_msg, "Switch Proxy") );
+}
+
+test "Broken statusline is detected correctly in an HttpStatus" {
+ const status_line = "HTTP/1.1 306 \r\n";
+ const allocator = std.testing.allocator;
+ var fis = std.io.fixedBufferStream(status_line);
+ const reader = fis.reader();
+
+ var status = HttpStatusParser.init(allocator);
+ defer status.deinit();
+ try expectError( error.ParsingError, status.parseReader(reader) );
+}
+
+
+/// HttpHeaders
+
+pub const HttpHeaders = struct{
+ values: StringHashMap([]const u8),
+ owned_data: std.ArrayList([]u8),
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) Self{
+ return Self{
+ .values = StringHashMap([]const u8).init(allocator),
+ .allocator = allocator,
+ .owned_data = std.ArrayList([]u8).init(allocator),
+ };
+ }
+ pub fn get(self: *Self, k: []const u8) !?[]const u8{
+ var key:[]u8 = try self.allocator.alloc(u8, k.len);
+ defer self.allocator.free(key);
+ for (key) |*char, index|{
+ char.* = std.ascii.toLower(k[index]);
+ }
+ return self.values.get(key);
+ }
+ pub fn put(self: *Self, k: []const u8, v: []const u8) !void{
+ // Own key
+ var key:[]u8 = try self.allocator.alloc(u8, k.len);
+ for (key) |*char,index|{
+ char.* = std.ascii.toLower(char.*);
+ char.* = std.ascii.toLower(k[index]);
+ }
+ try self.owned_data.append(key);
+
+ // Own value
+ var value:[]u8 = try self.allocator.alloc(u8, v.len);
+ std.mem.copy(u8, value, v);
+ try self.owned_data.append(value);
+
+ // Store header
+ try self.values.put(key, value);
+ }
+ pub fn deinit(self: *Self) void{
+ self.values.deinit();
+ for (self.owned_data.items) |item|{
+ self.allocator.free(item);
+ }
+ self.owned_data.deinit();
+ }
+};
+
+pub const HttpHeaderParser = struct{
+ headers: HttpHeaders,
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) Self{
+ return Self{
+ .headers = HttpHeaders.init(allocator),
+ .allocator = allocator,
+ };
+ }
+ pub fn parseReader(self: *Self, reader: anytype) !void{
+ while(true) {
+ var line:[100000]u8 = undefined;
+ var slice = try reader.readUntilDelimiter(&line, '\r');
+ // Drop \n
+ _ = try reader.readByte();
+
+ if(slice.len == 0){
+ return; // Finished header block
+ }
+ var hit = std.mem.split(u8, slice, ":");
+ const key = if (hit.next()) |k| k else return error.ParsingError;
+ const val = hit.rest();
+ if (val.len == 0){
+ return error.ParsingError;
+ }
+ var spaces:usize = 0;
+ for(val) |ch|{
+ if(ch == ' '){
+ spaces = spaces + 1;
+ } else {
+ break;
+ }
+ } else {
+ return error.ParsingError;
+ }
+
+ try self.headers.put(key, val[spaces..]);
+ }
+ }
+ pub fn deinit(self: *Self) void{
+ self.headers.deinit();
+ }
+};
+
+
+
+test "Header block is parsed correctly, is case-insensitive and ignores spaces after :" {
+ const allocator = std.testing.allocator;
+
+ const block =
+ "Host: api.open-meteo.com\r\n" ++
+ "Transfer-Encoding: chunked\r\n" ++
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0\r\n" ++
+ "\r\n";
+ var fis = std.io.fixedBufferStream(block);
+ const reader = fis.reader();
+
+ var headers = HttpHeaderParser.init(allocator);
+ defer headers.deinit();
+
+ try headers.parseReader(reader);
+ if (try headers.headers.get("HOST")) |host| {
+ try expect( std.mem.eql(u8, host, "api.open-meteo.com") );
+ }
+ if (try headers.headers.get("host")) |host| {
+ try expect( std.mem.eql(u8, host, "api.open-meteo.com") );
+ }
+ if (try headers.headers.get("Transfer-Encoding")) |tencoding| {
+ try expect( std.mem.eql(u8, tencoding, "chunked") );
+ }
+ if (try headers.headers.get("User-Agent")) |useragent| {
+ try expect( std.mem.eql(u8, useragent,
+ "Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0") );
+ }
+}
+
+test "Header line with no semicolon is a parsing error" {
+ const allocator = std.testing.allocator;
+
+ const block =
+ "Host api.open-meteo.com\r\n" ++
+ "Transfer-Encoding: chunked\r\n" ++
+ "\r\n";
+ var fis = std.io.fixedBufferStream(block);
+ const reader = fis.reader();
+
+ var headers = HttpHeaderParser.init(allocator);
+ defer headers.deinit();
+
+ try expectError(error.ParsingError, headers.parseReader(reader));
+}
+
+
+/// HttpBody
+
+pub const HttpBodyParser = struct {
+ data: []u8,
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) !Self{
+ return Self{
+ .data = try allocator.create([0]u8),
+ .allocator = allocator,
+ };
+ }
+ pub fn deinit(self: *Self) void{
+ self.allocator.free(self.data);
+ }
+ pub fn parseChunkedReader(self: *Self, body_reader: anytype) !void{
+ var cursor:usize = 0;
+ while (true){
+ var buffer:[100]u8 = undefined;
+ // Read chunk size line
+ var size = try body_reader.readUntilDelimiter(&buffer, '\r');
+ _ = try body_reader.readByte(); // Drop \n
+ // Parse the number
+ const chunk_size = try std.fmt.parseUnsigned(usize, size, 16);
+ // If chunk is empty finish
+ if( chunk_size == 0 ){ return; }
+
+ // Reallocate the data to fit the incoming chunk
+ self.data = try self.allocator.realloc(self.data, cursor + chunk_size);
+ // Read all the bytes of the chunk
+ for( self.data[cursor..cursor+chunk_size] ) |*byte|{
+ byte.* = try body_reader.readByte();
+ }
+ // Drop \r\n
+ _ = try body_reader.readByte();
+ _ = try body_reader.readByte();
+ cursor = cursor + chunk_size;
+ }
+ }
+ pub fn parseFixedSizeReader(self: *Self, body_reader: anytype, size: usize) !void{
+ self.allocator.free(self.data);
+ self.data = try body_reader.readAllAlloc(self.allocator, size);
+ }
+};
+
+test "Parse chunked body from Reader"{
+ const chunked_body =
+ "7\r\n" ++
+ "Mozilla\r\n" ++
+ "12\r\n" ++
+ " Developer Network\r\n" ++
+ "0\r\n" ++
+ "\r\n".*;
+ var fis = std.io.fixedBufferStream(chunked_body);
+ const reader = fis.reader();
+
+ const allocator = std.testing.allocator;
+ var body = try HttpBodyParser.init(allocator);
+ defer body.deinit();
+
+ try body.parseChunkedReader(reader);
+
+ try expect( std.mem.eql(u8, "Mozilla Developer Network", body.data) );
+}
+
+/// HttpResponse
+
+pub const HttpResponseParser = struct{
+ statusparser: HttpStatusParser,
+ headerparser: HttpHeaderParser,
+ bodyparser: HttpBodyParser,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) !Self{
+ return Self{
+ .statusparser = HttpStatusParser.init(allocator),
+ .headerparser = HttpHeaderParser.init(allocator),
+ .bodyparser = try HttpBodyParser.init(allocator),
+ };
+ }
+ pub fn parseReader(self: *Self, reader: anytype) !void{
+ try self.statusparser.parseReader(reader);
+ try self.headerparser.parseReader(reader);
+ if(try self.headerparser.headers.get("Transfer-Encoding")) |te|{
+ if( std.mem.containsAtLeast(u8, te, 1, "chunked") ) {
+ try self.bodyparser.parseChunkedReader(reader);
+ }
+ } else {
+ if(try self.headerparser.headers.get("Content-Length")) |cl|{
+ const content_length = try std.fmt.parseUnsigned(usize, cl, 10);
+ try self.bodyparser.parseFixedSizeReader(reader, content_length);
+ }
+ }
+ }
+ pub fn deinit(self: *Self) void{
+ self.statusparser.deinit();
+ self.headerparser.deinit();
+ self.bodyparser.deinit();
+ }
+
+ pub fn body(self: Self) []u8{
+ return self.bodyparser.data;
+ }
+ pub fn headers(self: Self) HttpHeaders{
+ return self.headerparser.headers;
+ }
+ pub fn status(self: Self) usize{
+ return self.statusparser.status.status;
+ }
+ pub fn statusMsg(self: Self) []const u8{
+ return self.statusparser.status.status_msg;
+ }
+ pub fn protocol(self: Self) []const u8{
+ return self.statusparser.status.protocol;
+ }
+};
+
+// Full answer parser
+
+test "Parse full response correctly" {
+ const responsedata =
+ "HTTP/1.1 200 OK\r\n" ++
+ "Content-Type: text/plain\r\n" ++
+ "Transfer-Encoding: chunked\r\n" ++
+ "\r\n" ++
+ "7\r\n" ++
+ "Mozilla\r\n" ++
+ "12\r\n" ++
+ " Developer Network\r\n" ++
+ "0\r\n" ++
+ "\r\n";
+
+ var fis = std.io.fixedBufferStream(responsedata);
+ const reader = fis.reader();
+
+ const allocator = std.testing.allocator;
+ var response = try HttpResponseParser.init(allocator);
+ defer response.deinit();
+
+ try response.parseReader(reader);
+
+ try expect( std.mem.eql(u8, response.body(), "Mozilla Developer Network") );
+ try expect( response.status() == 200 );
+ try expect( std.mem.eql(u8, response.statusMsg(), "OK") );
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..7cf9729
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,39 @@
+const std = @import("std");
+const weatherFetcher = @import("weatherFetcher.zig");
+
+pub fn main() !void{
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer _ = gpa.deinit();
+ const allocator = gpa.allocator();
+
+ const args = try std.process.argsAlloc(allocator);
+ defer std.process.argsFree(allocator, args);
+
+ _ = try weatherFetcher.getData(allocator);
+
+ const stdout = std.io.getStdOut().writer();
+ try stdout.print("Weather app {s}\n", .{args});
+}
+
+//pub fn main() !void {
+// // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
+// std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
+//
+// // stdout is for the actual output of your application, for example if you
+// // are implementing gzip, then only the compressed bytes should be sent to
+// // stdout, not any debugging messages.
+// const stdout_file = std.io.getStdOut().writer();
+// var bw = std.io.bufferedWriter(stdout_file);
+// const stdout = bw.writer();
+//
+// try stdout.print("Run `zig build test` to run the tests.\n", .{});
+//
+// try bw.flush(); // don't forget to flush!
+//}
+//
+//test "simple test" {
+// var list = std.ArrayList(i32).init(std.testing.allocator);
+// defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
+// try list.append(42);
+// try std.testing.expectEqual(@as(i32, 42), list.pop());
+//}
diff --git a/src/weatherFetcher.zig b/src/weatherFetcher.zig
new file mode 100644
index 0000000..be99d5c
--- /dev/null
+++ b/src/weatherFetcher.zig
@@ -0,0 +1,101 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const net = std.net;
+
+const expect = std.testing.expect;
+
+const http = @import("./http.zig");
+
+
+pub const DailyDataSchema = struct {
+ latitude: f64,
+ longitude: f64,
+ generationtime_ms: f64,
+ utc_offset_seconds: i64,
+ timezone: []u8,
+ timezone_abbreviation: []u8,
+ elevation: f64,
+ hourly_units: struct{
+ time: []u8,
+ temperature_2m: []u8,
+ },
+ hourly: struct {
+ time: [][]u8,
+ temperature_2m: []f64,
+ },
+};
+
+pub const DailyData = struct{
+ data: DailyDataSchema,
+ allocator: std.mem.Allocator,
+
+ const Self = @This();
+ pub fn parse(allocator: Allocator, body: []u8) !Self{
+ var stream = std.json.TokenStream.init(body);
+ return Self{
+ .data = try std.json.parse(DailyDataSchema, &stream, .{
+ .allocator = allocator
+ }),
+ .allocator = allocator,
+ };
+ }
+ pub fn deinit(self: *Self) void{
+ std.json.parseFree(DailyDataSchema, self.data, .{
+ .allocator = self.allocator,
+ });
+ }
+};
+
+/// Gets Daily Data. Allocates and returns an DailyData struct that must be
+/// .deinit() later
+pub fn getDailyData(allocator: std.mem.Allocator) !DailyData {
+ const stream = try net.tcpConnectToHost(allocator, "api.open-meteo.com", 80);
+ defer stream.close();
+
+ const writer = stream.writer();
+ try writer.writeAll( "GET /v1/forecast" ++
+ "?latitude=52.52" ++
+ "&longitude=13.41" ++
+ "&hourly=temperature_2m" ++ " HTTP/1.1\r\n" ++
+ "Host: api.open-meteo.com\r\n" ++
+ "Connection: keep-alive\r\n" ++
+ "Accept-Encoding: identity\r\n" ++
+ "\r\n");
+ const reader = stream.reader();
+
+ var response = try http.HttpResponseParser.init(allocator);
+ defer response.deinit();
+
+ try response.parseReader(reader);
+ return try DailyData.parse(allocator, response.body());
+}
+
+test "Parse a daily data block correctly with DailyDataSchema" {
+ const allocator = std.testing.allocator;
+
+ const body =
+\\ {"latitude":52.52,"longitude":13.419998,"generationtime_ms":0.4259347915649414,
+\\ "utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation":"GMT",
+\\ "elevation":38.0,"hourly_units":
+\\ {"time":"iso8601","temperature_2m":"°C"},
+\\ "hourly":{"time":["2023-04-09T00:00","2023-04-09T01:00"],"temperature_2m":[4.3,3.6]}}
+;
+ var stream = std.json.TokenStream.init(body);
+ const parsedData = try std.json.parse(DailyDataSchema, &stream, .{
+ .allocator = allocator
+ });
+ defer std.json.parseFree(DailyDataSchema, parsedData, .{
+ .allocator = allocator
+ });
+
+ try expect( std.mem.eql(u8, parsedData.timezone_abbreviation, "GMT") );
+ try expect( std.mem.eql(u8, parsedData.hourly.time[0], "2023-04-09T00:00") );
+}
+
+test "Get data properly and parse it (requires access to open-meteo)" {
+ const allocator = std.testing.allocator;
+
+ var daily = try getDailyData(allocator);
+ defer daily.deinit();
+ try expect( std.mem.eql(u8, daily.data.hourly_units.time, "iso8601") );
+}