diff --git a/src/http/handler.zig b/src/http/handler.zig index 246882e..f34825a 100644 --- a/src/http/handler.zig +++ b/src/http/handler.zig @@ -140,32 +140,33 @@ fn handleWeatherInternal( const coords_header = try std.fmt.allocPrint(res.arena, "{d:.4},{d:.4}", .{ location.coords.latitude, location.coords.longitude }); res.headers.add("X-Location-Coordinates", coords_header); - res.body = blk: { - if (params.format) |fmt| { - // Anything except the json will be plain text - res.content_type = .TEXT; - if (std.mem.eql(u8, fmt, "j1")) { - res.content_type = .JSON; // reset to json - break :blk try Json.render(req_alloc, weather); - } - if (std.mem.eql(u8, fmt, "p1")) - break :blk try Prometheus.render(req_alloc, weather); - if (std.mem.eql(u8, fmt, "v2")) - break :blk try V2.render(req_alloc, weather, render_options.use_imperial); - if (std.mem.startsWith(u8, fmt, "%")) - break :blk try Custom.render(req_alloc, weather, fmt, render_options.use_imperial); - // fall back to line if we don't understand the format parameter - break :blk try Line.render(req_alloc, weather, fmt, render_options.use_imperial); + if (params.format) |fmt| { + // Anything except the json will be plain text + res.content_type = .TEXT; + if (std.mem.eql(u8, fmt, "j1")) { + res.content_type = .JSON; // reset to json + try Json.render(res.writer(), weather); + } else if (std.mem.eql(u8, fmt, "p1")) { + try Prometheus.render(res.writer(), weather, req_alloc); + } else if (std.mem.eql(u8, fmt, "v2")) { + try V2.render(res.writer(), weather, render_options.use_imperial); + } else if (std.mem.startsWith(u8, fmt, "%")) { + try Custom.render(res.writer(), weather, fmt, render_options.use_imperial); } else { - render_options.format = determineFormat(params, req.headers.get("user-agent")); - log.debug( - "Format: {}. params.ansi {}, params.text {}, user agent: {?s}", - .{ render_options.format, params.ansi, params.text_only, req.headers.get("user-agent") }, - ); - if (render_options.format != .html) res.content_type = .TEXT else res.content_type = .HTML; - break :blk try Formatted.render(req_alloc, weather, render_options); + // fall back to line if we don't understand the format parameter + try Line.render(res.writer(), weather, fmt, render_options.use_imperial); } - }; + } else { + // No specific format selected, we'll provide Formatted output in either + // text (ansi/plain) or html + render_options.format = determineFormat(params, req.headers.get("user-agent")); + log.debug( + "Format: {}. params.ansi {}, params.text {}, user agent: {?s}", + .{ render_options.format, params.ansi, params.text_only, req.headers.get("user-agent") }, + ); + if (render_options.format != .html) res.content_type = .TEXT else res.content_type = .HTML; + try Formatted.render(res.writer(), weather, render_options); + } } fn determineFormat(params: QueryParams, user_agent: ?[]const u8) Formatted.Format { diff --git a/src/render/Custom.zig b/src/render/Custom.zig index b3f0694..e396e60 100644 --- a/src/render/Custom.zig +++ b/src/render/Custom.zig @@ -8,18 +8,14 @@ const Astronomical = @import("../Astronomical.zig"); const TimeZoneOffsets = @import("../location/timezone_offsets.zig"); const Coordinates = @import("../Coordinates.zig"); -pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format: []const u8, use_imperial: bool) ![]const u8 { - var output: std.ArrayList(u8) = .empty; - errdefer output.deinit(allocator); - const writer = output.writer(allocator); - +pub fn render(writer: *std.Io.Writer, weather: types.WeatherData, format: []const u8, use_imperial: bool) !void { var i: usize = 0; while (i < format.len) { if (format[i] == '%' and i + 1 < format.len) { const code = format[i + 1]; switch (code) { - 'c' => try writer.print("{s}", .{emoji.getWeatherEmoji(weather.current.weather_code)}), - 'C' => try writer.print("{s}", .{weather.current.condition}), + 'c' => try writer.writeAll(emoji.getWeatherEmoji(weather.current.weather_code)), + 'C' => try writer.writeAll(weather.current.condition), 'h' => try writer.print("{d}%", .{weather.current.humidity}), 't' => { const temp = if (use_imperial) weather.current.tempFahrenheit() else weather.current.temp_c; @@ -46,7 +42,7 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format: const unit = if (use_imperial) "mph" else "km/h"; try writer.print("{d:.0} {s} {s}", .{ wind, unit, utils.degreeToDirection(weather.current.wind_deg) }); }, - 'l' => try writer.print("{s}", .{weather.locationDisplayName()}), + 'l' => try writer.writeAll(weather.locationDisplayName()), 'p' => { const precip = if (use_imperial) weather.current.precip_mm * 0.0393701 else weather.current.precip_mm; const unit = if (use_imperial) "in" else "mm"; @@ -60,7 +56,7 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format: 'm' => { const now = try nowAt(weather.coords); const moon = Moon.getPhase(now); - try writer.print("{s}", .{moon.emoji()}); + try writer.writeAll(moon.emoji()); }, 'M' => { const now = try nowAt(weather.coords); @@ -101,8 +97,6 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format: i += 1; } } - - return output.toOwnedSlice(allocator); } fn nowAt(coords: Coordinates) !i64 { @@ -137,8 +131,12 @@ test "render custom format with location and temp" { .allocator = allocator, }; - const output = try render(allocator, weather, "%l: %c %t", false); - defer allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, "%l: %c %t", false); + + const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "London") != null); try std.testing.expect(std.mem.indexOf(u8, output, "+7.0°C") != null); @@ -166,8 +164,12 @@ test "render custom format with newline" { .allocator = allocator, }; - const output = try render(allocator, weather, "%l%n%C", false); - defer allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, "%l%n%C", false); + + const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "Paris\nClear") != null); } @@ -194,8 +196,12 @@ test "render custom format with humidity and pressure" { .allocator = allocator, }; - const output = try render(allocator, weather, "Humidity: %h, Pressure: %P", false); - defer allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, "Humidity: %h, Pressure: %P", false); + + const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "85%") != null); try std.testing.expect(std.mem.indexOf(u8, output, "1012") != null); @@ -223,8 +229,12 @@ test "render custom format with imperial units" { .allocator = allocator, }; - const output = try render(allocator, weather, "%t %w %p", true); - defer allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, "%t %w %p", true); + + const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "+50.0°F") != null); try std.testing.expect(std.mem.indexOf(u8, output, "mph") != null); diff --git a/src/render/Formatted.zig b/src/render/Formatted.zig index 06c704c..892aa39 100644 --- a/src/render/Formatted.zig +++ b/src/render/Formatted.zig @@ -106,11 +106,8 @@ pub const RenderOptions = struct { format: Format = .ansi, }; -pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: RenderOptions) ![]const u8 { - var output = std.Io.Writer.Allocating.init(allocator); - defer output.deinit(); - - const w = &output.writer; +pub fn render(writer: *std.Io.Writer, data: types.WeatherData, options: RenderOptions) !void { + const w = writer; if (options.format == .html) try w.writeAll("
");
     if (!options.super_quiet)
         try w.print(
@@ -128,8 +125,6 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: Re
         }
     }
     if (options.format == .html) try w.writeAll("
"); - - return output.toOwnedSlice(); } fn renderCurrent(w: *std.Io.Writer, current: types.CurrentCondition, options: RenderOptions) !void { @@ -678,8 +673,12 @@ test "render with imperial units" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{ .use_imperial = true }); - defer std.testing.allocator.free(output); + var output_buf: [4096]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, data, .{ .use_imperial = true }); + + const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "+50") != null); try std.testing.expect(std.mem.indexOf(u8, output, "°F") != null); @@ -737,12 +736,12 @@ test "partly cloudy weather art" { fn testArt(data: types.WeatherData) !void { inline for (std.meta.fields(Format)) |f| { const format: Format = @enumFromInt(f.value); - const output = try render( - std.testing.allocator, - data, - .{ .format = format }, - ); - defer std.testing.allocator.free(output); + var output_buf: [8192]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, data, .{ .format = format }); + + const output = output_buf[0..writer.end]; const target = getWeatherArt( data.current.weather_code, @@ -944,15 +943,23 @@ test "temperature matches between ansi and custom format" { .allocator = std.testing.allocator, }; - const ansi_output = try render(std.testing.allocator, data, .{ .use_imperial = true }); - defer std.testing.allocator.free(ansi_output); + var ansi_buf: [4096]u8 = undefined; + var ansi_writer = std.Io.Writer.fixed(&ansi_buf); - const custom_output = try custom.render(std.testing.allocator, data, "%t", true); - defer std.testing.allocator.free(custom_output); + try render(&ansi_writer, data, .{ .use_imperial = true }); + + const ansi_output = ansi_buf[0..ansi_writer.end]; + + var custom_buf: [1024]u8 = undefined; + var custom_writer = std.Io.Writer.fixed(&custom_buf); + + try custom.render(&custom_writer, data, "%t", true); + + const output = custom_buf[0..custom_writer.end]; // ANSI rounds to integer, custom shows decimal try std.testing.expect(std.mem.indexOf(u8, ansi_output, "+56") != null); - try std.testing.expect(std.mem.indexOf(u8, custom_output, "55.6°F") != null); + try std.testing.expect(std.mem.indexOf(u8, output, "55.6°F") != null); } test "tempColor returns correct colors for temperature ranges" { @@ -1037,8 +1044,12 @@ test "plain text format - MetNo real data" { const weather_data = try MetNo.parse(undefined, allocator, json_data); defer weather_data.deinit(); - const output = try render(allocator, weather_data, .{ .format = .plain_text, .days = 3 }); - defer allocator.free(output); + var output_buf: [8192]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather_data, .{ .format = .plain_text, .days = 3 }); + + const output = output_buf[0..writer.end]; const expected = \\Weather report: 47.6038,-122.3301 @@ -1245,8 +1256,12 @@ test "ansi format - MetNo real data - phoenix" { const weather_data = try MetNo.parse(undefined, allocator, json_data); defer weather_data.deinit(); - const output = try render(allocator, weather_data, .{ .format = .ansi, .days = 3, .use_imperial = true }); - defer allocator.free(output); + var output_buf: [16384]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather_data, .{ .format = .ansi, .days = 3, .use_imperial = true }); + + const output = output_buf[0..writer.end]; const expected = @embedFile("../tests/metno-phoenix.ansi"); diff --git a/src/render/Json.zig b/src/render/Json.zig index bb3a294..cc9f4b0 100644 --- a/src/render/Json.zig +++ b/src/render/Json.zig @@ -1,7 +1,7 @@ const std = @import("std"); const types = @import("../weather/types.zig"); -pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const u8 { +pub fn render(writer: *std.Io.Writer, weather: types.WeatherData) !void { const data = .{ .current_condition = .{ .temp_C = weather.current.temp_c, @@ -16,7 +16,7 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const .weather = weather.forecast, }; - return try std.fmt.allocPrint(allocator, "{f}", .{std.json.fmt(data, .{})}); + try writer.print("{f}", .{std.json.fmt(data, .{})}); } test "render json format" { @@ -41,8 +41,12 @@ test "render json format" { .allocator = allocator, }; - const output = try render(allocator, weather); - defer allocator.free(output); + var output_buf: [4096]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather); + + const output = output_buf[0..writer.end]; try std.testing.expect(output.len > 0); try std.testing.expect(std.mem.indexOf(u8, output, "temp_C") != null); diff --git a/src/render/Line.zig b/src/render/Line.zig index 15359b7..5942b86 100644 --- a/src/render/Line.zig +++ b/src/render/Line.zig @@ -3,11 +3,11 @@ const types = @import("../weather/types.zig"); const emoji = @import("emoji.zig"); const utils = @import("utils.zig"); -pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []const u8, use_imperial: bool) ![]const u8 { +pub fn render(writer: *std.Io.Writer, data: types.WeatherData, format: []const u8, use_imperial: bool) !void { if (std.mem.eql(u8, format, "1")) { const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; const unit = if (use_imperial) "°F" else "°C"; - return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s}", .{ + try writer.print("{s}: {s} {d:.0}{s}", .{ data.location, emoji.getWeatherEmoji(data.current.weather_code), temp, @@ -18,7 +18,7 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []c const unit = if (use_imperial) "°F" else "°C"; const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; const wind_unit = if (use_imperial) "mph" else "km/h"; - return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{ + try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{ data.location, emoji.getWeatherEmoji(data.current.weather_code), temp, @@ -33,7 +33,7 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []c const unit = if (use_imperial) "°F" else "°C"; const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; const wind_unit = if (use_imperial) "mph" else "km/h"; - return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%%", .{ + try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%%", .{ data.location, emoji.getWeatherEmoji(data.current.weather_code), temp, @@ -50,7 +50,7 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []c const unit = if (use_imperial) "°F" else "°C"; const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; const wind_unit = if (use_imperial) "mph" else "km/h"; - return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%% {s}", .{ + try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%% {s}", .{ data.location, emoji.getWeatherEmoji(data.current.weather_code), temp, @@ -64,58 +64,53 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []c "☀️", }); } else { - return renderCustom(allocator, data, format, use_imperial); + try renderCustom(writer, data, format, use_imperial); } } -fn renderCustom(allocator: std.mem.Allocator, data: types.WeatherData, format: []const u8, use_imperial: bool) ![]const u8 { - var output: std.ArrayList(u8) = .empty; - errdefer output.deinit(allocator); - +fn renderCustom(writer: *std.Io.Writer, data: types.WeatherData, format: []const u8, use_imperial: bool) !void { var i: usize = 0; while (i < format.len) { if (format[i] == '%' and i + 1 < format.len) { const code = format[i + 1]; switch (code) { - 'c' => try output.appendSlice(allocator, emoji.getWeatherEmoji(data.current.weather_code)), - 'C' => try output.appendSlice(allocator, data.current.condition), - 'h' => try output.writer(allocator).print("{d}", .{data.current.humidity}), + 'c' => try writer.writeAll(emoji.getWeatherEmoji(data.current.weather_code)), + 'C' => try writer.writeAll(data.current.condition), + 'h' => try writer.print("{d}", .{data.current.humidity}), 't' => { const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - try output.writer(allocator).print("{d:.0}", .{temp}); + try writer.print("{d:.0}", .{temp}); }, 'f' => { const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - try output.writer(allocator).print("{d:.0}", .{temp}); + try writer.print("{d:.0}", .{temp}); }, 'w' => { const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; const wind_unit = if (use_imperial) "mph" else "km/h"; - try output.writer(allocator).print("{s}{d:.0}{s}", .{ utils.degreeToDirection(data.current.wind_deg), wind, wind_unit }); + try writer.print("{s}{d:.0}{s}", .{ utils.degreeToDirection(data.current.wind_deg), wind, wind_unit }); }, - 'l' => try output.appendSlice(allocator, data.location), + 'l' => try writer.writeAll(data.location), 'p' => { const precip = if (use_imperial) data.current.precip_mm * 0.0393701 else data.current.precip_mm; - try output.writer(allocator).print("{d:.1}", .{precip}); + try writer.print("{d:.1}", .{precip}); }, 'P' => { const pressure = if (use_imperial) data.current.pressure_mb * 0.02953 else data.current.pressure_mb; - try output.writer(allocator).print("{d:.0}", .{pressure}); + try writer.print("{d:.0}", .{pressure}); }, - '%' => try output.append(allocator, '%'), + '%' => try writer.writeByte('%'), else => { - try output.append(allocator, '%'); - try output.append(allocator, code); + try writer.writeByte('%'); + try writer.writeByte(code); }, } i += 2; } else { - try output.append(allocator, format[i]); + try writer.writeByte(format[i]); i += 1; } } - - return output.toOwnedSlice(allocator); } test "format 1" { @@ -138,8 +133,12 @@ test "format 1" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, "1", false); - defer std.testing.allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, data, "1", false); + + const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("London: ☀️ 15°C", output); } @@ -164,8 +163,12 @@ test "custom format" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, "%l: %c %t°C", false); - defer std.testing.allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, data, "%l: %c %t°C", false); + + const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("London: ☀️ 15°C", output); } @@ -190,8 +193,12 @@ test "format 2 with imperial units" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, "2", true); - defer std.testing.allocator.free(output); + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, data, "2", true); + + const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("Portland: ☁️ 50°F 🌬️SE12mph", output); } diff --git a/src/render/Prometheus.zig b/src/render/Prometheus.zig index 94ecdde..eecd3fd 100644 --- a/src/render/Prometheus.zig +++ b/src/render/Prometheus.zig @@ -1,11 +1,9 @@ const std = @import("std"); const types = @import("../weather/types.zig"); const Moon = @import("../Moon.zig"); +const utils = @import("utils.zig"); -pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const u8 { - var output: std.ArrayList(u8) = .empty; - errdefer output.deinit(allocator); - const writer = output.writer(allocator); +pub fn render(writer: *std.Io.Writer, weather: types.WeatherData, allocator: std.mem.Allocator) !void { // Current conditions try writer.print("# HELP temperature_feels_like_celsius Feels Like Temperature in Celsius\n", .{}); @@ -59,7 +57,7 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const try writer.print("weather_desc{{forecast=\"current\", description=\"{s}\"}} 1\n", .{weather.current.condition}); try writer.print("# HELP winddir_16_point Wind Direction on a 16-wind compass rose\n", .{}); - const wind_dir = degreeToDirection(weather.current.wind_deg); + const wind_dir = utils.degreeToDirection(weather.current.wind_deg); try writer.print("winddir_16_point{{forecast=\"current\", description=\"{s}\"}} 1\n", .{wind_dir}); // Forecast days @@ -109,15 +107,6 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const try writer.print("# HELP astronomy_sunset_min Minutes since start of the day until the moon disappears below the horizon\n", .{}); try writer.print("astronomy_sunset_min{{forecast=\"{s}\"}} 0\n", .{forecast_label}); // Not calculated } - - return output.toOwnedSlice(allocator); -} - -fn degreeToDirection(degrees: f64) []const u8 { - const normalized = @mod(degrees + 11.25, 360); - const index: usize = @intFromFloat(normalized / 22.5); - const directions = [_][]const u8{ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" }; - return directions[index]; } test "prometheus format includes required metrics" { @@ -153,8 +142,12 @@ test "prometheus format includes required metrics" { .allocator = allocator, }; - const output = try render(allocator, weather); - defer allocator.free(output); + var output_buf: [8192]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, allocator); + + const output = output_buf[0..writer.end]; // Check for key metrics try std.testing.expect(std.mem.indexOf(u8, output, "temperature_celsius{forecast=\"current\"}") != null); @@ -186,8 +179,12 @@ test "prometheus format has proper help comments" { .allocator = allocator, }; - const output = try render(allocator, weather); - defer allocator.free(output); + var output_buf: [4096]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, allocator); + + const output = output_buf[0..writer.end]; // Check for HELP comments try std.testing.expect(std.mem.indexOf(u8, output, "# HELP temperature_celsius Temperature in Celsius") != null); diff --git a/src/render/V2.zig b/src/render/V2.zig index 3366868..1e12b4e 100644 --- a/src/render/V2.zig +++ b/src/render/V2.zig @@ -2,11 +2,7 @@ const std = @import("std"); const types = @import("../weather/types.zig"); const utils = @import("utils.zig"); -pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, use_imperial: bool) ![]const u8 { - var output: std.ArrayList(u8) = .empty; - errdefer output.deinit(allocator); - const writer = output.writer(allocator); - +pub fn render(writer: *std.Io.Writer, weather: types.WeatherData, use_imperial: bool) !void { // Header with location try writer.print("Weather report: {s}\n\n", .{weather.locationDisplayName()}); @@ -62,8 +58,6 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, use_impe } } } - - return output.toOwnedSlice(allocator); } test "render v2 format" { @@ -88,8 +82,12 @@ test "render v2 format" { .allocator = allocator, }; - const output = try render(allocator, weather, false); - defer allocator.free(output); + var output_buf: [2048]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, false); + + const output = output_buf[0..writer.end]; try std.testing.expect(output.len > 0); try std.testing.expect(std.mem.indexOf(u8, output, "Munich") != null); @@ -119,8 +117,12 @@ test "render v2 format with imperial units" { .allocator = allocator, }; - const output = try render(allocator, weather, true); - defer allocator.free(output); + var output_buf: [2048]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, weather, true); + + const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "50.0°F") != null); try std.testing.expect(std.mem.indexOf(u8, output, "mph") != null);