- Zig 90.1%
- Nix 9.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The package was named zig_geojson, so a dependent wrote
b.dependency("zig_geojson", ...) to reach a module called geojson. It is
now geojson throughout. The fingerprint's checksum half follows the new
name; its id half is unchanged, so this is the same package and not a fork.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JPx2vKtR1Mc1Upt4s25ZtS
|
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-geojson
GeoJSON (RFC 7946) for Zig 0.17,
read and written with std.json.
Every type here is an ordinary std.json type with the hooks that make it
parse and stringify as GeoJSON. So a Geometry or a Feature can be a field
of anything else std.json reads, such as an API response that embeds GeoJSON
in a document of its own. Positions are
zig-geo's Position, so a geometry
read from a document can be asked where it is, what box it fits in and what
it contains, with no conversion.
The API documentation is generated from the doc comments, which carry most of
the explanation. The workflow publishes it from main to
https://jeff.jcollie.page/zig-geojson/.
Where this lives
The repository is in three places, all with the same history. The Forgejo instance at https://git.jcollie.dev/jeff/zig-geojson is the one with a web view:
$ git clone https://git.jcollie.dev/jeff/zig-geojson.git
It is mirrored on Tangled at https://tangled.org/jcollie.dev/zig-geojson. It is also on the Radicle network, with the repository identifier
rad:z2x9jTH1mKqtKcGYsWyePiHXJWCkZ
rad clone rad:z2x9jTH1mKqtKcGYsWyePiHXJWCkZ fetches it from any node that seeds it. Any of the three is
the whole project.
Adding it to a project
$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-geojson
const geojson = b.dependency("geojson", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("geojson", geojson.module("geojson"));
It depends on zig-geo, and re-exports it as geojson.geo, so a program has
zig-geo's functions without a dependency of its own.
For Zig 0.16.0, use the zig-0.16 branch, which holds the last of
zig-geojson to build with it. The v0.1.0 tag is the same commit:
$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-geojson#zig-0.16
The package is named zig_geojson there, so that is the name to give
b.dependency.
Using it
const geojson = @import("geojson");
const Properties = struct { name: []const u8 };
const parsed = try std.json.parseFromSlice(geojson.FeatureCollection(Properties), gpa, text, .{});
defer parsed.deinit();
const here: geojson.Position = .{ .longitude = -93.625, .latitude = 41.5868 };
for (parsed.value.features) |feature| {
const area = feature.geometry orelse continue;
if (area.contains(here)) std.debug.print("{s}\n", .{feature.properties.name});
}
try std.json.Stringify.value(parsed.value, .{}, writer);
| Type | What it is |
|---|---|
Geometry |
one of RFC 7946's seven kinds, from point to geometry_collection |
Feature(P) |
a geometry (or none), an id, a bbox, and properties of type P |
FeatureCollection(P) |
features, and a bbox |
Object(P) |
any GeoJSON text, told apart by its type |
The properties' type is the caller's choice:
- a struct, for documents whose shape is known;
std.json.Value, for documents whose shape isn't;?T, when features may have no properties at all.
A Geometry can say whether it contains a position (the areal kinds only)
and what its boundingBox is. clone gives it an allocator of its own, which
is how a geometry is kept after the parse's arena is freed.
What is read, and what is refused
RFC 7946 asks readers to be lenient in some places and not others, and this reader follows it:
- Foreign members (§6.1) on a Feature or a collection are passed over,
whatever
ignore_unknown_fieldssays. The National Weather Service, for one, puts@contexton everything. A feature's properties are parsed with the caller's options, because their shape is the caller's business. - Positions: a position may carry more than three numbers. The third is the altitude and the rest are dropped (§3.1.1).
- Rings are read whether or not they close. RFC 7946's MUSTs about shape
(lines at least two positions, rings at least four and closed, positions in
range) are checked separately by
Geometry.validate. Its SHOULD about ring orientation isGeometry.followsRightHandRule. typeis required to tell a geometry's kind or anObjectapart. On aFeatureor aFeatureCollectionit is checked if present and not required.- Refused: an unknown geometry type, coordinates nested to the wrong depth,
a number too large to be finite (
1e999), and GeometryCollections nested more than 16 deep. RFC 7946 §3.1.8 says they shouldn't nest at all, and the limit keeps a hostile document from exhausting the stack. - Numeric ids: a whole-number
idis read as an integer however it is spelled, so-1e2and-100are the same id. The fuzzer found that without this an id didn't read back the same after being written.
A geometry's own bbox isn't kept. It says nothing its coordinates don't, and
boundingBox computes it. A Feature's or a collection's bbox is kept.
Building and testing
Everything runs inside the flake's devshell:
$ nix develop
$ zig build test --summary all # unit tests, the RFC's examples, and the fuzz seeds
$ zig build check # also compiles the fuzz driver and the docs server
$ zig fmt --check --exclude zig-pkg .
$ reuse lint
$ nix build # the package, built and tested offline
tests/rfc7946/ holds the examples from RFC 7946 §1.5 and Appendix A,
extracted from the RFC as they stand. Every one is read, validated, written
and read back. Writing it a second time must produce the same bytes.
Fuzzing
The fuzz target reads arbitrary bytes as GeoJSON. Whatever it reads must write back out and read back as the same thing, and validating it, measuring it and testing it for containment must neither panic nor leak. Zig's own fuzzer runs it with coverage feedback:
$ zig build fuzz --fuzz # until interrupted, with a web interface
$ zig build fuzz --fuzz=1M # a bounded run, then a report
The test binaries are compiled with LLVM (use_llvm in build.zig), since
the self-hosted backend Debug otherwise uses emits no coverage
instrumentation. tools/fuzz.zig drives the same targets with a mutation loop
of its own, bounded by a count so that CI runs the same inputs every time:
$ zig build fuzz-run -- --iterations 60000 # what CI runs
$ zig build fuzz-run -- --seconds 600
$ zig build fuzz-run -- --input fuzz-findings/x.bin --target objects
API documentation
$ zig build docs # into zig-out/docs
$ zig build docs-serve # and read them at http://127.0.0.1:8000/
The docs have to be served rather than opened as files. The viewer is a
WebAssembly program that fetches its sources at runtime, and a browser refuses
that from a file:// page.
References cited
Butler, H., M. Daly, A. Doyle, S. Gillies, S. Hagen, and T. Schaub. 2016.
The GeoJSON Format. RFC 7946. RFC Editor.
https://www.rfc-editor.org/info/rfc7946. The format, and the source of the
examples in tests/rfc7946/.
IETF Trust. 2015. "Legal Provisions Relating to IETF Documents." Version 5.0, corrected 2021. https://trustee.ietf.org/documents/trust-legal-provisions/. §4.c licenses the Code Components of an IETF document, including the examples above, under the Revised BSD licence. RFCs of RFC 7946's era call it the "Simplified BSD License"; the Trust has since corrected that name.
Licence
MIT, and REUSE compliant: every file says what it
is licensed under, and reuse lint checks that. The examples in
tests/rfc7946/ are the IETF's, under BSD-3-Clause, as REUSE.toml records.