move to std.Io.Writer rather than allocating strings everywhere

This commit is contained in:
Emil Lerch 2026-01-07 09:11:26 -08:00
parent b46940a7a3
commit 17d94d9285
Signed by: lobo
GPG key ID: A7B62D657EF764F8
7 changed files with 167 additions and 131 deletions

View file

@ -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);
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 {
// 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;
break :blk try Formatted.render(req_alloc, weather, render_options);
try Formatted.render(res.writer(), weather, render_options);
}
};
}
fn determineFormat(params: QueryParams, user_agent: ?[]const u8) Formatted.Format {

View file

@ -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);

View file

@ -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("<pre>");
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("</pre>");
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");

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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);