AI: add imperial/US default
This commit is contained in:
parent
4746ad81b9
commit
bcab444bca
5 changed files with 95 additions and 2 deletions
|
|
@ -157,7 +157,7 @@ curl wttr.in/London?Tnq
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `A` | Force ANSI output (even for browsers) |
|
| `A` | Force ANSI output (even for browsers) |
|
||||||
| `n` | Narrow output (narrower terminal width) |
|
| `n` | Narrow output (narrower terminal width) |
|
||||||
| `m` | Use metric units |
|
| `m` | Use metric units (force metric even for US) |
|
||||||
| `M` | Use m/s for wind speed |
|
| `M` | Use m/s for wind speed |
|
||||||
| `u` | Use imperial units (Fahrenheit, mph) |
|
| `u` | Use imperial units (Fahrenheit, mph) |
|
||||||
| `I` | Inverted colors |
|
| `I` | Inverted colors |
|
||||||
|
|
@ -172,6 +172,14 @@ curl wttr.in/London?Tnq
|
||||||
| `Q` | Super quiet (no city name) |
|
| `Q` | Super quiet (no city name) |
|
||||||
| `F` | No follow line (no "Follow @igor_chubin") |
|
| `F` | No follow line (no "Follow @igor_chubin") |
|
||||||
|
|
||||||
|
**Unit System Defaults:**
|
||||||
|
|
||||||
|
The service automatically selects units based on:
|
||||||
|
1. Explicit query parameter (`?u` or `?m`) - highest priority
|
||||||
|
2. Language parameter (`lang=us`) - forces imperial
|
||||||
|
3. Client IP geolocation - US IPs default to imperial
|
||||||
|
4. Default - metric for all other locations
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```bash
|
```bash
|
||||||
curl wttr.in/London?T # Plain text, no ANSI
|
curl wttr.in/London?T # Plain text, no ANSI
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ This directory contains comprehensive documentation for rewriting wttr.in in Zig
|
||||||
- Static help pages (/:help, /:translation)
|
- Static help pages (/:help, /:translation)
|
||||||
- Error handling (404/500 status codes)
|
- Error handling (404/500 status codes)
|
||||||
- Configuration from environment variables
|
- Configuration from environment variables
|
||||||
|
- **Imperial units auto-detection**: Automatically uses imperial units (°F, mph) for US IP addresses and `lang=us`, with explicit `?u` and `?m` overrides
|
||||||
|
|
||||||
### Missing Features (To Be Implemented Later)
|
### Missing Features (To Be Implemented Later)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ pub const HandleWeatherOptions = struct {
|
||||||
cache: *Cache,
|
cache: *Cache,
|
||||||
provider: WeatherProvider,
|
provider: WeatherProvider,
|
||||||
resolver: *Resolver,
|
resolver: *Resolver,
|
||||||
|
geoip: *@import("../location/geoip.zig").GeoIP,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn handleWeather(
|
pub fn handleWeather(
|
||||||
|
|
@ -138,7 +139,25 @@ fn handleWeatherInternal(
|
||||||
if (params.lang) |l| allocator.free(l);
|
if (params.lang) |l| allocator.free(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
const use_imperial = if (params.units) |u| u == .uscs else false;
|
// Determine if imperial units should be used
|
||||||
|
// Priority: explicit ?u or ?m > lang=us > US IP > default metric
|
||||||
|
const use_imperial = blk: {
|
||||||
|
if (params.units) |u| {
|
||||||
|
break :blk u == .uscs;
|
||||||
|
}
|
||||||
|
if (params.lang) |lang| {
|
||||||
|
if (std.mem.eql(u8, lang, "us")) {
|
||||||
|
break :blk true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const client_ip = getClientIP(req);
|
||||||
|
if (client_ip.len > 0) {
|
||||||
|
if (opts.geoip.isUSIP(client_ip)) {
|
||||||
|
break :blk true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk false;
|
||||||
|
};
|
||||||
|
|
||||||
const output = if (params.format) |fmt| blk: {
|
const output = if (params.format) |fmt| blk: {
|
||||||
if (std.mem.eql(u8, fmt, "j1")) {
|
if (std.mem.eql(u8, fmt, "j1")) {
|
||||||
|
|
@ -190,3 +209,25 @@ test "parseXForwardedFor trims whitespace" {
|
||||||
test "parseXForwardedFor handles empty string" {
|
test "parseXForwardedFor handles empty string" {
|
||||||
try std.testing.expectEqualStrings("", parseXForwardedFor(""));
|
try std.testing.expectEqualStrings("", parseXForwardedFor(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "imperial units selection logic" {
|
||||||
|
// This test documents the priority order for unit selection:
|
||||||
|
// 1. Explicit ?u or ?m parameter (highest priority)
|
||||||
|
// 2. lang=us parameter
|
||||||
|
// 3. US IP detection
|
||||||
|
// 4. Default to metric
|
||||||
|
|
||||||
|
// The actual logic is tested through integration tests
|
||||||
|
// This test just verifies the QueryParams parsing works
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
const params_u = try QueryParams.parse(allocator, "u");
|
||||||
|
try std.testing.expectEqual(QueryParams.Units.uscs, params_u.units.?);
|
||||||
|
|
||||||
|
const params_m = try QueryParams.parse(allocator, "m");
|
||||||
|
try std.testing.expectEqual(QueryParams.Units.metric, params_m.units.?);
|
||||||
|
|
||||||
|
const params_lang = try QueryParams.parse(allocator, "lang=us");
|
||||||
|
defer allocator.free(params_lang.lang.?);
|
||||||
|
try std.testing.expectEqualStrings("us", params_lang.lang.?);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,32 @@ pub const GeoIP = struct {
|
||||||
return try self.extractCoordinates(result.entry);
|
return try self.extractCoordinates(result.entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn isUSIP(self: *GeoIP, ip: []const u8) bool {
|
||||||
|
const ip_z = std.heap.c_allocator.dupeZ(u8, ip) catch return false;
|
||||||
|
defer std.heap.c_allocator.free(ip_z);
|
||||||
|
|
||||||
|
var gai_error: c_int = 0;
|
||||||
|
var mmdb_error: c_int = 0;
|
||||||
|
|
||||||
|
const result = MMDB_lookup_string(&self.mmdb, ip_z.ptr, &gai_error, &mmdb_error);
|
||||||
|
|
||||||
|
if (gai_error != 0 or mmdb_error != 0 or !result.found_entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry_mut = result.entry;
|
||||||
|
var country_data: MMDBEntryData = undefined;
|
||||||
|
const null_term: [*:0]const u8 = @ptrCast(&[_]u8{0});
|
||||||
|
const status = MMDB_get_value(&entry_mut, &country_data, "country\x00", "iso_code\x00", null_term);
|
||||||
|
|
||||||
|
if (status != 0 or !country_data.has_data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const country_code = std.mem.span(country_data.utf8_string);
|
||||||
|
return std.mem.eql(u8, country_code, "US");
|
||||||
|
}
|
||||||
|
|
||||||
fn extractCoordinates(self: *GeoIP, entry: MMDBEntry) !Coordinates {
|
fn extractCoordinates(self: *GeoIP, entry: MMDBEntry) !Coordinates {
|
||||||
_ = self;
|
_ = self;
|
||||||
var entry_mut = entry;
|
var entry_mut = entry;
|
||||||
|
|
@ -143,3 +169,19 @@ test "GeoIP init with invalid path fails" {
|
||||||
const result = GeoIP.init("/nonexistent/path.mmdb");
|
const result = GeoIP.init("/nonexistent/path.mmdb");
|
||||||
try std.testing.expectError(error.CannotOpenDatabase, result);
|
try std.testing.expectError(error.CannotOpenDatabase, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "isUSIP detects US IPs" {
|
||||||
|
var geoip = GeoIP.init("./GeoLite2-City.mmdb") catch {
|
||||||
|
std.debug.print("Skipping test - GeoLite2-City.mmdb not found\n", .{});
|
||||||
|
return error.SkipZigTest;
|
||||||
|
};
|
||||||
|
defer geoip.deinit();
|
||||||
|
|
||||||
|
// Test that the function doesn't crash with various IPs
|
||||||
|
_ = geoip.isUSIP("8.8.8.8");
|
||||||
|
_ = geoip.isUSIP("1.1.1.1");
|
||||||
|
|
||||||
|
// Test invalid IP returns false
|
||||||
|
const invalid = geoip.isUSIP("invalid");
|
||||||
|
try std.testing.expect(!invalid);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ pub fn main() !void {
|
||||||
.cache = &cache,
|
.cache = &cache,
|
||||||
.provider = metno.provider(),
|
.provider = metno.provider(),
|
||||||
.resolver = &resolver,
|
.resolver = &resolver,
|
||||||
|
.geoip = &geoip,
|
||||||
}, &rate_limiter);
|
}, &rate_limiter);
|
||||||
|
|
||||||
try server.listen();
|
try server.listen();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue