diff --git a/src/http/QueryParams.zig b/src/http/QueryParams.zig new file mode 100644 index 0000000..8cb36fa --- /dev/null +++ b/src/http/QueryParams.zig @@ -0,0 +1,151 @@ +//!Units: +//! +//! m # metric (SI) (used by default everywhere except US) +//! u # USCS (used by default in US) +//! M # show wind speed in m/s +//! +//!View options: +//! +//! 0 # only current weather +//! 1 # current weather + today's forecast +//! 2 # current weather + today's + tomorrow's forecast +//! A # ignore User-Agent and force ANSI output format (terminal) +//! d # restrict output to standard console font glyphs +//! F # do not show the "Follow" line +//! n # narrow version (only day and night) +//! q # quiet version (no "Weather report" text) +//! Q # superquiet version (no "Weather report", no city name) +//! T # switch terminal sequences off (no colors) + +const std = @import("std"); +const RenderOptions = @import("../render/Formatted.zig").RenderOptions; + +const QueryParams = @This(); + +format: ?[]const u8 = null, +lang: ?[]const u8 = null, +location: ?[]const u8 = null, +transparency: ?u8 = null, +/// A: Ignore user agent and force ansi mode +ansi: bool = false, +/// T: Avoid terminal sequences and just output plain text +text_only: bool = false, +/// This is necessary because it it imporant to know if the user explicitly +/// requested imperial/metric +use_imperial: ?bool = null, +render_options: RenderOptions, + +pub fn parse(allocator: std.mem.Allocator, query_string: []const u8) !QueryParams { + // SAFETY: function adds render_options at end of function before return + var params = QueryParams{ .render_options = undefined }; + var iter = std.mem.splitScalar(u8, query_string, '&'); + var render_options = RenderOptions{}; + while (iter.next()) |pair| { + if (pair.len == 0) continue; + + var kv = std.mem.splitScalar(u8, pair, '='); + const key = kv.next() orelse continue; + const value = kv.next(); + + if (key.len == 1) { + switch (key[0]) { + '0' => render_options.days = 0, + '1' => render_options.days = 1, + '2' => render_options.days = 2, + 'u' => params.use_imperial = true, + 'm' => params.use_imperial = false, + 'n' => render_options.narrow = true, + 'q' => render_options.quiet = true, + 'Q' => render_options.super_quiet = true, + 'A' => params.ansi = true, + 'T' => params.text_only = true, + 't' => params.transparency = 150, + else => continue, + } + } + if (std.mem.eql(u8, key, "format")) { + params.format = if (value) |v| try allocator.dupe(u8, v) else null; + } else if (std.mem.eql(u8, key, "lang")) { + params.lang = if (value) |v| try allocator.dupe(u8, v) else null; + } else if (std.mem.eql(u8, key, "location")) { + params.location = if (value) |v| try allocator.dupe(u8, v) else null; + } else if (std.mem.eql(u8, key, "use_imperial")) { + params.use_imperial = true; + } else if (std.mem.eql(u8, key, "use_metric")) { + params.use_imperial = false; + } else if (std.mem.eql(u8, key, "transparency")) { + if (value) |v| { + params.transparency = try std.fmt.parseInt(u8, v, 10); + } + } + } + + if (params.use_imperial) |u| render_options.use_imperial = u; + params.render_options = render_options; + return params; +} + +test "parse empty query" { + const allocator = std.testing.allocator; + const params = try QueryParams.parse(allocator, ""); + try std.testing.expect(params.format == null); + try std.testing.expect(params.lang == null); + try std.testing.expect(params.use_imperial == null); +} + +test "parse format parameter" { + const allocator = std.testing.allocator; + const params = try QueryParams.parse(allocator, "format=j1"); + defer if (params.format) |f| allocator.free(f); + try std.testing.expect(params.format != null); + try std.testing.expectEqualStrings("j1", params.format.?); +} + +test "parse units with question mark" { + const allocator = std.testing.allocator; + + // Test with just "u" (no question mark in query string) + const params1 = try QueryParams.parse(allocator, "u"); + try std.testing.expect(params1.use_imperial.?); + + // Test with "u=" (empty value) + const params2 = try QueryParams.parse(allocator, "u="); + try std.testing.expect(params2.use_imperial.?); + + // Test combined with other params + const params3 = try QueryParams.parse(allocator, "format=3&u"); + defer if (params3.format) |f| allocator.free(f); + try std.testing.expect(params3.use_imperial.?); +} + +test "parse units parameters" { + const allocator = std.testing.allocator; + const params_m = try QueryParams.parse(allocator, "m"); + try std.testing.expect(!params_m.use_imperial.?); + + const params_u = try QueryParams.parse(allocator, "u"); + try std.testing.expect(params_u.use_imperial.?); + + const params_u_query = try QueryParams.parse(allocator, "u="); + try std.testing.expect(params_u_query.use_imperial.?); +} + +test "parse multiple parameters" { + const allocator = std.testing.allocator; + const params = try QueryParams.parse(allocator, "format=3&lang=de&m"); + defer if (params.format) |f| allocator.free(f); + defer if (params.lang) |l| allocator.free(l); + try std.testing.expectEqualStrings("3", params.format.?); + try std.testing.expectEqualStrings("de", params.lang.?); + try std.testing.expect(!params.use_imperial.?); +} + +test "parse transparency" { + const allocator = std.testing.allocator; + const params_t = try QueryParams.parse(allocator, "t"); + try std.testing.expect(params_t.transparency != null); + try std.testing.expectEqual(@as(u8, 150), params_t.transparency.?); + + const params_custom = try QueryParams.parse(allocator, "transparency=200"); + try std.testing.expectEqual(@as(u8, 200), params_custom.transparency.?); +} diff --git a/src/http/handler.zig b/src/http/handler.zig index c581635..cd6e170 100644 --- a/src/http/handler.zig +++ b/src/http/handler.zig @@ -2,7 +2,7 @@ const std = @import("std"); const httpz = @import("httpz"); const WeatherProvider = @import("../weather/Provider.zig"); const Resolver = @import("../location/resolver.zig").Resolver; -const QueryParams = @import("query.zig").QueryParams; +const QueryParams = @import("QueryParams.zig"); const Formatted = @import("../render/Formatted.zig"); const Line = @import("../render/Line.zig"); const Json = @import("../render/Json.zig"); diff --git a/src/http/query.zig b/src/http/query.zig deleted file mode 100644 index e7920aa..0000000 --- a/src/http/query.zig +++ /dev/null @@ -1,150 +0,0 @@ -const std = @import("std"); -const RenderOptions = @import("../render/Formatted.zig").RenderOptions; - -///Units: -/// -/// m # metric (SI) (used by default everywhere except US) -/// u # USCS (used by default in US) -/// M # show wind speed in m/s -/// -///View options: -/// -/// 0 # only current weather -/// 1 # current weather + today's forecast -/// 2 # current weather + today's + tomorrow's forecast -/// A # ignore User-Agent and force ANSI output format (terminal) -/// d # restrict output to standard console font glyphs -/// F # do not show the "Follow" line -/// n # narrow version (only day and night) -/// q # quiet version (no "Weather report" text) -/// Q # superquiet version (no "Weather report", no city name) -/// T # switch terminal sequences off (no colors) -pub const QueryParams = struct { - format: ?[]const u8 = null, - lang: ?[]const u8 = null, - location: ?[]const u8 = null, - transparency: ?u8 = null, - /// A: Ignore user agent and force ansi mode - ansi: bool = false, - /// T: Avoid terminal sequences and just output plain text - text_only: bool = false, - /// This is necessary because it it imporant to know if the user explicitly - /// requested imperial/metric - use_imperial: ?bool = null, - render_options: RenderOptions, - - pub fn parse(allocator: std.mem.Allocator, query_string: []const u8) !QueryParams { - // SAFETY: function adds render_options at end of function before return - var params = QueryParams{ .render_options = undefined }; - var iter = std.mem.splitScalar(u8, query_string, '&'); - var render_options = RenderOptions{}; - while (iter.next()) |pair| { - if (pair.len == 0) continue; - - var kv = std.mem.splitScalar(u8, pair, '='); - const key = kv.next() orelse continue; - const value = kv.next(); - - if (key.len == 1) { - switch (key[0]) { - '0' => render_options.days = 0, - '1' => render_options.days = 1, - '2' => render_options.days = 2, - 'u' => params.use_imperial = true, - 'm' => params.use_imperial = false, - 'n' => render_options.narrow = true, - 'q' => render_options.quiet = true, - 'Q' => render_options.super_quiet = true, - 'A' => params.ansi = true, - 'T' => params.text_only = true, - 't' => params.transparency = 150, - else => continue, - } - } - if (std.mem.eql(u8, key, "format")) { - params.format = if (value) |v| try allocator.dupe(u8, v) else null; - } else if (std.mem.eql(u8, key, "lang")) { - params.lang = if (value) |v| try allocator.dupe(u8, v) else null; - } else if (std.mem.eql(u8, key, "location")) { - params.location = if (value) |v| try allocator.dupe(u8, v) else null; - } else if (std.mem.eql(u8, key, "use_imperial")) { - params.use_imperial = true; - } else if (std.mem.eql(u8, key, "use_metric")) { - params.use_imperial = false; - } else if (std.mem.eql(u8, key, "transparency")) { - if (value) |v| { - params.transparency = try std.fmt.parseInt(u8, v, 10); - } - } - } - - if (params.use_imperial) |u| render_options.use_imperial = u; - params.render_options = render_options; - return params; - } -}; - -test "parse empty query" { - const allocator = std.testing.allocator; - const params = try QueryParams.parse(allocator, ""); - try std.testing.expect(params.format == null); - try std.testing.expect(params.lang == null); - try std.testing.expect(params.use_imperial == null); -} - -test "parse format parameter" { - const allocator = std.testing.allocator; - const params = try QueryParams.parse(allocator, "format=j1"); - defer if (params.format) |f| allocator.free(f); - try std.testing.expect(params.format != null); - try std.testing.expectEqualStrings("j1", params.format.?); -} - -test "parse units with question mark" { - const allocator = std.testing.allocator; - - // Test with just "u" (no question mark in query string) - const params1 = try QueryParams.parse(allocator, "u"); - try std.testing.expect(params1.use_imperial.?); - - // Test with "u=" (empty value) - const params2 = try QueryParams.parse(allocator, "u="); - try std.testing.expect(params2.use_imperial.?); - - // Test combined with other params - const params3 = try QueryParams.parse(allocator, "format=3&u"); - defer if (params3.format) |f| allocator.free(f); - try std.testing.expect(params3.use_imperial.?); -} - -test "parse units parameters" { - const allocator = std.testing.allocator; - const params_m = try QueryParams.parse(allocator, "m"); - try std.testing.expect(!params_m.use_imperial.?); - - const params_u = try QueryParams.parse(allocator, "u"); - try std.testing.expect(params_u.use_imperial.?); - - const params_u_query = try QueryParams.parse(allocator, "u="); - try std.testing.expect(params_u_query.use_imperial.?); -} - -test "parse multiple parameters" { - const allocator = std.testing.allocator; - const params = try QueryParams.parse(allocator, "format=3&lang=de&m"); - defer if (params.format) |f| allocator.free(f); - defer if (params.lang) |l| allocator.free(l); - try std.testing.expectEqualStrings("3", params.format.?); - try std.testing.expectEqualStrings("de", params.lang.?); - try std.testing.expect(!params.use_imperial.?); -} - -test "parse transparency" { - const allocator = std.testing.allocator; - const params_t = try QueryParams.parse(allocator, "t"); - try std.testing.expect(params_t.transparency != null); - try std.testing.expectEqual(@as(u8, 150), params_t.transparency.?); - - const params_custom = try QueryParams.parse(allocator, "transparency=200"); - try std.testing.expectEqual(@as(u8, 200), params_custom.transparency.?); -} diff --git a/src/main.zig b/src/main.zig index 05d93c8..e75ecc5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -90,7 +90,7 @@ test { _ = @import("cache/Lru.zig"); _ = @import("weather/Mock.zig"); _ = @import("http/RateLimiter.zig"); - _ = @import("http/query.zig"); + _ = @import("http/QueryParams.zig"); _ = @import("http/help.zig"); _ = @import("render/Formatted.zig"); _ = @import("render/Line.zig");