Skip to content

Commit 0b0cf76

Browse files
committed
Add Country and ASN records with basic tests
1 parent 3d42348 commit 0b0cf76

File tree

2 files changed

+232
-39
lines changed

2 files changed

+232
-39
lines changed

src/geolite2.zig

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,97 @@
11
const std = @import("std");
22

3-
// City represents a record in the GeoLite2-City database, for example,
4-
// https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json.
5-
pub const City = struct {
6-
city: struct {
7-
geoname_id: u32 = 0,
8-
names: ?std.hash_map.StringHashMap([]const u8) = null,
9-
},
10-
continent: struct {
3+
pub const StringMap = std.hash_map.StringHashMap([]const u8);
4+
5+
/// Country represents a record in the GeoLite2-Country database, for example,
6+
/// https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-Country-Test.json.
7+
/// It can be used for geolocation at the country-level for analytics, content customization,
8+
/// or compliance use cases in territories that are not disputed.
9+
pub const Country = struct {
10+
continent: Self.Continent,
11+
country: Self.Country,
12+
registered_country: Self.Country,
13+
represented_country: Self.RepresentedCountry,
14+
15+
_arena: std.heap.ArenaAllocator,
16+
17+
const Self = @This();
18+
pub const Continent = struct {
1119
code: []const u8 = "",
1220
geoname_id: u32 = 0,
13-
names: ?std.hash_map.StringHashMap([]const u8) = null,
14-
},
15-
country: struct {
21+
names: ?StringMap = null,
22+
};
23+
pub const Country = struct {
1624
geoname_id: u32 = 0,
1725
is_in_european_union: bool = false,
1826
iso_code: []const u8 = "",
19-
names: ?std.hash_map.StringHashMap([]const u8) = null,
20-
},
21-
location: struct {
27+
names: ?StringMap = null,
28+
};
29+
pub const RepresentedCountry = struct {
30+
geoname_id: u32 = 0,
31+
is_in_european_union: bool = false,
32+
iso_code: []const u8 = "",
33+
names: ?StringMap = null,
34+
type: []const u8 = "",
35+
};
36+
37+
pub fn init(allocator: std.mem.Allocator) Self {
38+
const arena = std.heap.ArenaAllocator.init(allocator);
39+
40+
return .{
41+
.continent = .{},
42+
.country = .{},
43+
.registered_country = .{},
44+
.represented_country = .{},
45+
46+
._arena = arena,
47+
};
48+
}
49+
50+
pub fn deinit(self: *const Self) void {
51+
self._arena.deinit();
52+
}
53+
};
54+
55+
/// City represents a record in the GeoLite2-City database, for example,
56+
/// https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json.
57+
/// It can be used for geolocation down to the city or postal code for analytics and content customization.
58+
pub const City = struct {
59+
city: Self.City,
60+
continent: Country.Continent,
61+
country: Country.Country,
62+
location: Self.Location,
63+
postal: Self.Postal,
64+
registered_country: Country.Country,
65+
represented_country: Country.RepresentedCountry,
66+
subdivisions: ?std.ArrayList(Self.Subdivision) = null,
67+
68+
_arena: std.heap.ArenaAllocator,
69+
70+
const Self = @This();
71+
pub const City = struct {
72+
geoname_id: u32 = 0,
73+
names: ?StringMap = null,
74+
};
75+
pub const Location = struct {
2276
accuracy_radius: u16 = 0,
2377
latitude: f64 = 0,
2478
longitude: f64 = 0,
2579
metro_code: u16 = 0,
2680
time_zone: []const u8 = "",
27-
},
28-
postal: struct {
81+
};
82+
pub const Postal = struct {
2983
code: []const u8 = "",
30-
},
31-
registered_country: struct {
84+
};
85+
pub const Subdivision = struct {
3286
geoname_id: u32 = 0,
33-
is_in_european_union: bool = false,
3487
iso_code: []const u8 = "",
35-
names: ?std.hash_map.StringHashMap([]const u8) = null,
36-
},
37-
represented_country: struct {
38-
geoname_id: u32 = 0,
39-
is_in_european_union: bool = false,
40-
iso_code: []const u8 = "",
41-
names: ?std.hash_map.StringHashMap([]const u8) = null,
42-
type: []const u8 = "",
43-
},
44-
subdivisions: ?std.ArrayList(struct {
45-
geoname_id: u32 = 0,
46-
iso_code: []const u8 = "",
47-
names: ?std.hash_map.StringHashMap([]const u8) = null,
48-
}) = null,
49-
50-
_arena: std.heap.ArenaAllocator,
88+
names: ?StringMap = null,
89+
};
5190

52-
pub fn init(allocator: std.mem.Allocator) City {
91+
pub fn init(allocator: std.mem.Allocator) Self {
5392
const arena = std.heap.ArenaAllocator.init(allocator);
5493

55-
return City{
94+
return .{
5695
.city = .{},
5796
.continent = .{},
5897
.country = .{},
@@ -65,7 +104,14 @@ pub const City = struct {
65104
};
66105
}
67106

68-
pub fn deinit(self: *const City) void {
107+
pub fn deinit(self: *const Self) void {
69108
self._arena.deinit();
70109
}
71110
};
111+
112+
/// Provides the autonomous system number and organization for IP addresses for analytics,
113+
/// e.g., https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json.
114+
pub const ASN = struct {
115+
autonomous_system_number: u32 = 0,
116+
autonomous_system_organization: []const u8 = "",
117+
};

src/maxminddb.zig

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const std = @import("std");
2+
13
const reader = @import("reader.zig");
24
const decoder = @import("decoder.zig");
35
const net = @import("net.zig");
@@ -10,5 +12,150 @@ pub const Metadata = reader.Metadata;
1012
pub const Network = net.Network;
1113

1214
test {
13-
@import("std").testing.refAllDecls(@This());
15+
std.testing.refAllDecls(@This());
16+
}
17+
18+
fn expectEqualMaps(
19+
map: geolite2.StringMap,
20+
keys: []const []const u8,
21+
values: []const []const u8,
22+
) !void {
23+
try std.testing.expectEqual(map.count(), keys.len);
24+
25+
for (keys, values) |key, want_value| {
26+
const got_value = map.get(key) orelse {
27+
std.debug.print("map key=\"{s}\" was not found\n", .{key});
28+
return error.MapKeyNotFound;
29+
};
30+
try std.testing.expectEqualStrings(want_value, got_value);
31+
}
32+
}
33+
34+
const expectEqual = std.testing.expectEqual;
35+
const expectEqualStrings = std.testing.expectEqualStrings;
36+
37+
test "GeoLite2 Country" {
38+
var db = try Reader.open_mmap(
39+
std.testing.allocator,
40+
"test-data/test-data/GeoLite2-Country-Test.mmdb",
41+
);
42+
defer db.close();
43+
44+
const ip = try std.net.Address.parseIp("89.160.20.128", 0);
45+
const got = try db.lookup(geolite2.Country, &ip);
46+
defer got.deinit();
47+
48+
try expectEqualStrings("EU", got.continent.code);
49+
try expectEqual(6255148, got.continent.geoname_id);
50+
try expectEqualMaps(
51+
got.continent.names.?,
52+
&.{ "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN" },
53+
&.{ "Europa", "Europe", "Europa", "Europe", "ヨーロッパ", "Europa", "Европа", "欧洲" },
54+
);
55+
56+
try expectEqual(2661886, got.country.geoname_id);
57+
try expectEqual(true, got.country.is_in_european_union);
58+
try expectEqualStrings("SE", got.country.iso_code);
59+
try expectEqualMaps(
60+
got.country.names.?,
61+
&.{ "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN" },
62+
&.{ "Schweden", "Sweden", "Suecia", "Suède", "スウェーデン王国", "Suécia", "Швеция", "瑞典" },
63+
);
64+
65+
try expectEqual(2921044, got.registered_country.geoname_id);
66+
try expectEqual(true, got.registered_country.is_in_european_union);
67+
try expectEqualStrings("DE", got.registered_country.iso_code);
68+
try expectEqualMaps(
69+
got.registered_country.names.?,
70+
&.{ "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN" },
71+
&.{ "Deutschland", "Germany", "Alemania", "Allemagne", "ドイツ連邦共和国", "Alemanha", "Германия", "德国" },
72+
);
73+
74+
try std.testing.expectEqualDeep(geolite2.Country.RepresentedCountry{}, got.represented_country);
75+
}
76+
77+
test "GeoLite2 City" {
78+
var db = try Reader.open_mmap(
79+
std.testing.allocator,
80+
"test-data/test-data/GeoLite2-City-Test.mmdb",
81+
);
82+
defer db.close();
83+
84+
const ip = try std.net.Address.parseIp("89.160.20.128", 0);
85+
const got = try db.lookup(geolite2.City, &ip);
86+
defer got.deinit();
87+
88+
try expectEqual(2694762, got.city.geoname_id);
89+
try expectEqualMaps(
90+
got.city.names.?,
91+
&.{ "de", "en", "fr", "ja", "zh-CN" },
92+
&.{ "Linköping", "Linköping", "Linköping", "リンシェーピング", "林雪平" },
93+
);
94+
95+
try expectEqualStrings("EU", got.continent.code);
96+
try expectEqual(6255148, got.continent.geoname_id);
97+
try expectEqualMaps(
98+
got.continent.names.?,
99+
&.{ "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN" },
100+
&.{ "Europa", "Europe", "Europa", "Europe", "ヨーロッパ", "Europa", "Европа", "欧洲" },
101+
);
102+
103+
try expectEqual(2661886, got.country.geoname_id);
104+
try expectEqual(true, got.country.is_in_european_union);
105+
try expectEqualStrings("SE", got.country.iso_code);
106+
try expectEqualMaps(
107+
got.country.names.?,
108+
&.{ "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN" },
109+
&.{ "Schweden", "Sweden", "Suecia", "Suède", "スウェーデン王国", "Suécia", "Швеция", "瑞典" },
110+
);
111+
112+
try std.testing.expectEqualDeep(
113+
geolite2.City.Location{
114+
.accuracy_radius = 76,
115+
.latitude = 58.4167,
116+
.longitude = 15.6167,
117+
.time_zone = "Europe/Stockholm",
118+
},
119+
got.location,
120+
);
121+
122+
try std.testing.expectEqualDeep(geolite2.City.Postal{}, got.postal);
123+
124+
try expectEqual(2921044, got.registered_country.geoname_id);
125+
try expectEqual(true, got.registered_country.is_in_european_union);
126+
try expectEqualStrings("DE", got.registered_country.iso_code);
127+
try expectEqualMaps(
128+
got.registered_country.names.?,
129+
&.{ "de", "en", "es", "fr", "ja", "pt-BR", "ru", "zh-CN" },
130+
&.{ "Deutschland", "Germany", "Alemania", "Allemagne", "ドイツ連邦共和国", "Alemanha", "Германия", "德国" },
131+
);
132+
133+
try std.testing.expectEqualDeep(geolite2.Country.RepresentedCountry{}, got.represented_country);
134+
135+
try expectEqual(1, got.subdivisions.?.items.len);
136+
const sub = got.subdivisions.?.getLast();
137+
try expectEqual(2685867, sub.geoname_id);
138+
try expectEqualStrings("E", sub.iso_code);
139+
try expectEqualMaps(
140+
sub.names.?,
141+
&.{ "en", "fr" },
142+
&.{ "Östergötland County", "Comté d'Östergötland" },
143+
);
144+
}
145+
146+
test "GeoLite2 ASN" {
147+
var db = try Reader.open_mmap(
148+
std.testing.allocator,
149+
"test-data/test-data/GeoLite2-ASN-Test.mmdb",
150+
);
151+
defer db.close();
152+
153+
const ip = try std.net.Address.parseIp("89.160.20.128", 0);
154+
const got = try db.lookup(geolite2.ASN, &ip);
155+
156+
const want = geolite2.ASN{
157+
.autonomous_system_number = 29518,
158+
.autonomous_system_organization = "Bredband2 AB",
159+
};
160+
try std.testing.expectEqualDeep(want, got);
14161
}

0 commit comments

Comments
 (0)