tg_geometry 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -79
- data/README.md +82 -191
- data/Rakefile +3 -3
- data/benchmark/falcon_concurrency.rb +1 -1
- data/benchmark/feature_source.rb +92 -0
- data/docs/ARCHITECTURE.md +29 -107
- data/docs/BENCHMARKING.md +20 -1
- data/docs/CASUAL_EXAMPLE.md +71 -458
- data/docs/CONCURRENCY.md +13 -7
- data/docs/ERROR_HANDLING.md +30 -0
- data/docs/FEATURE_SOURCE.md +166 -0
- data/docs/LIMITATIONS.md +11 -50
- data/docs/MEMORY_OWNERSHIP.md +20 -2
- data/ext/tg_geometry/extconf.rb +46 -4
- data/ext/tg_geometry/tg_geometry_ext.c +2453 -150
- data/ext/tg_geometry/tg_geometry_vendor_json.c +17 -0
- data/ext/tg_geometry/tg_geometry_vendor_tg.c +3 -0
- data/ext/tg_geometry/vendor/.vendored +8 -2
- data/ext/tg_geometry/vendor/json/LICENSE +20 -0
- data/ext/tg_geometry/vendor/json/VERSION +3 -0
- data/ext/tg_geometry/vendor/json/json.c +1024 -0
- data/ext/tg_geometry/vendor/json/json.h +207 -0
- data/lib/tg/geometry/registry.rb +3 -3
- data/lib/tg/geometry/version.rb +1 -1
- data/script/vendor_libs.rb +22 -6
- data/spec/{expansion_a_auto_strategy_spec.rb → auto_strategy_spec.rb} +1 -1
- data/spec/{block_12_batch_packed_spec.rb → batch_packed_spec.rb} +1 -1
- data/spec/{block_20_concurrency_spec.rb → concurrency_spec.rb} +1 -1
- data/spec/{block_13_error_hardening_spec.rb → error_hardening_spec.rb} +1 -1
- data/spec/feature_source_nogvl_spec.rb +51 -0
- data/spec/feature_source_spec.rb +268 -0
- data/spec/{expansion_d_format_coverage_spec.rb → format_coverage_spec.rb} +1 -1
- data/spec/{block_20_fuzz_spec.rb → fuzz_spec.rb} +1 -1
- data/spec/{block_4_geom_api_spec.rb → geom_api_spec.rb} +1 -1
- data/spec/{block_3_geom_parse_spec.rb → geom_parse_spec.rb} +1 -1
- data/spec/{block_8_index_borrowed_geometry_spec.rb → index_borrowed_geometry_spec.rb} +1 -1
- data/spec/{block_6_index_build_spec.rb → index_build_spec.rb} +2 -2
- data/spec/{block_9_flat_query_spec.rb → index_flat_query_spec.rb} +1 -1
- data/spec/{block_7_index_owned_geometry_spec.rb → index_owned_geometry_spec.rb} +1 -1
- data/spec/{block_10_rtree_strategy_spec.rb → index_rtree_accounting_spec.rb} +1 -1
- data/spec/{block_11_rtree_order_spec.rb → index_rtree_order_spec.rb} +1 -1
- data/spec/{block_1_skeleton_spec.rb → load_and_errors_spec.rb} +1 -1
- data/spec/{expansion_e_low_level_geometry_spec.rb → low_level_geometry_spec.rb} +1 -1
- data/spec/{block_14_memory_gc_hardening_spec.rb → memory_gc_spec.rb} +1 -1
- data/spec/{expansion_i_ractor_spec.rb → ractor_spec.rb} +1 -1
- data/spec/{block_5_rect_api_spec.rb → rect_api_spec.rb} +1 -1
- data/spec/{expansion_b_registry_spec.rb → registry_spec.rb} +1 -1
- data/spec/{expansion_j_full_tg_api_coverage_spec.rb → tg_api_coverage_spec.rb} +1 -1
- data/spec/{block_2_vendor_spec.rb → vendor_sources_spec.rb} +4 -4
- metadata +39 -38
- data/docs/ACTIVE_RECORD.md +0 -26
- data/docs/AUTO_STRATEGY.md +0 -15
- data/docs/EXPANSION_E_TO_H_STATUS.md +0 -51
- data/docs/FORMAT_COVERAGE.md +0 -23
- data/docs/FULL_TG_API_COVERAGE.md +0 -109
- data/docs/LOW_LEVEL_GEOMETRY.md +0 -121
- data/docs/RACTOR.md +0 -40
- data/docs/REGISTRY.md +0 -37
- data/docs/RELEASE_CHECKLIST.md +0 -39
- /data/spec/{expansion_c_active_record_source_spec.rb → active_record_source_spec.rb} +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// https://github.com/tidwall/json.c
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2023 Joshua J Baker. All rights reserved.
|
|
4
|
+
// Use of this source code is governed by an MIT-style
|
|
5
|
+
// license that can be found in the LICENSE file.
|
|
6
|
+
#ifndef JSON_H
|
|
7
|
+
#define JSON_H
|
|
8
|
+
|
|
9
|
+
#include <stddef.h>
|
|
10
|
+
#include <stdint.h>
|
|
11
|
+
#include <stdbool.h>
|
|
12
|
+
|
|
13
|
+
enum json_type {
|
|
14
|
+
JSON_NULL,
|
|
15
|
+
JSON_FALSE,
|
|
16
|
+
JSON_NUMBER,
|
|
17
|
+
JSON_STRING,
|
|
18
|
+
JSON_TRUE,
|
|
19
|
+
JSON_ARRAY,
|
|
20
|
+
JSON_OBJECT,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
struct json { void *priv[4]; };
|
|
24
|
+
|
|
25
|
+
struct json_valid {
|
|
26
|
+
bool valid;
|
|
27
|
+
size_t pos;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// json_valid returns true if the input is valid json data.
|
|
31
|
+
bool json_valid(const char *json_str);
|
|
32
|
+
bool json_validn(const char *json_str, size_t len);
|
|
33
|
+
struct json_valid json_valid_ex(const char *json_str, int opts);
|
|
34
|
+
struct json_valid json_validn_ex(const char *json_str, size_t len, int opts);
|
|
35
|
+
|
|
36
|
+
// json_parse parses the input data and returns a json value.
|
|
37
|
+
//
|
|
38
|
+
// This function expects that the json is well-formed, and does not validate.
|
|
39
|
+
// Invalid json is safe and will not cause program errors, but it may return
|
|
40
|
+
// unexpected results. If you are consuming JSON from an unpredictable source
|
|
41
|
+
// then you may want to use the 'json_valid' function first.
|
|
42
|
+
//
|
|
43
|
+
// It's important to note that the resulting 'struct json' is backed by the
|
|
44
|
+
// same memory as json_str, thus a 'struct json' cannot outlive the original
|
|
45
|
+
// json_str. Attempting to use any of the json_*(struct json...) functions on
|
|
46
|
+
// the result, beyond the lifetime of the originating 'json_str', then it will
|
|
47
|
+
// likely result in a memory fault or some other type of undefined corruption.
|
|
48
|
+
struct json json_parse(const char *json_str);
|
|
49
|
+
struct json json_parsen(const char *json_str, size_t len);
|
|
50
|
+
|
|
51
|
+
// json_first returns the json's first child.
|
|
52
|
+
//
|
|
53
|
+
// For objects, the key of the first value is returned. For arrays, the first
|
|
54
|
+
// child value is returned. If the input json has no children, or is not an
|
|
55
|
+
// object or array, then a non-existent json value is returned.
|
|
56
|
+
struct json json_first(struct json json);
|
|
57
|
+
|
|
58
|
+
// json_next returns the json's next child.
|
|
59
|
+
//
|
|
60
|
+
// This typically follows a 'json_first' call, and is used to iterate over the
|
|
61
|
+
// children in an object or array. For arrays, each 'json_next' call will
|
|
62
|
+
// return a the next value. For objects, each 'json_next' call alternates over
|
|
63
|
+
// value, key, value key, value, etc. If there are no more children, then a
|
|
64
|
+
// non-existent json value is returned.
|
|
65
|
+
struct json json_next(struct json json);
|
|
66
|
+
|
|
67
|
+
// json_exists checks for the existence of json.
|
|
68
|
+
bool json_exists(struct json json);
|
|
69
|
+
|
|
70
|
+
// json_type returns the json's type.
|
|
71
|
+
//
|
|
72
|
+
// Returns one of the following: JSON_NULL, JSON_STRING, JSON_NUMBER,
|
|
73
|
+
// JSON_TRUE, JSON_FALSE, JSON_OBJECT, JSON_ARRAY.
|
|
74
|
+
// For non-existent json, JSON_NULL is returned.
|
|
75
|
+
enum json_type json_type(struct json json);
|
|
76
|
+
|
|
77
|
+
// json_raw returns the start of the raw json data.
|
|
78
|
+
//
|
|
79
|
+
// This function may return NULL if the input is non-existent. Also there is
|
|
80
|
+
// no guarentee that this data will be null-terminated C string. If you want to
|
|
81
|
+
// retain this data for longer that the data from the original 'json_parse'
|
|
82
|
+
// call then you should copy it first, such as:
|
|
83
|
+
//
|
|
84
|
+
// size_t len = json_raw_length(json);
|
|
85
|
+
// char *raw = malloc(len+1);
|
|
86
|
+
// memcpy(raw, json_raw(json), len);
|
|
87
|
+
// raw[len] = '\0';
|
|
88
|
+
//
|
|
89
|
+
const char *json_raw(struct json json);
|
|
90
|
+
|
|
91
|
+
// json_raw_length returns the length of the raw json data.
|
|
92
|
+
size_t json_raw_length(struct json json);
|
|
93
|
+
|
|
94
|
+
// Compare a json's raw data with a C string.
|
|
95
|
+
//
|
|
96
|
+
// This function performs a binary comparison of the characters. It compares
|
|
97
|
+
// each character, one-by-one, of both strings. If they are equal to each
|
|
98
|
+
// other, it continues until the characters differ or until the end of the JSON
|
|
99
|
+
// or a terminating null-character in the C string is reached.
|
|
100
|
+
// Returns -1, 0, +1 for less-than, equal-to, greater-than.
|
|
101
|
+
int json_raw_compare(struct json json, const char *str);
|
|
102
|
+
int json_raw_comparen(struct json json, const char *str, size_t len);
|
|
103
|
+
|
|
104
|
+
// Compares a json string to the provided C string.
|
|
105
|
+
//
|
|
106
|
+
// This function performs a binary comparison of the characters. It compares
|
|
107
|
+
// each character, one-by-one, of both strings. If they are equal to each
|
|
108
|
+
// other, it continues until the characters differ or until the end of the JSON
|
|
109
|
+
// or a terminating null-character in the C string is reached.
|
|
110
|
+
//
|
|
111
|
+
// If the JSON type is JSON_STRING, then each character that needs to be
|
|
112
|
+
// unescaped is done so in a streaming manner. ie. without the additional
|
|
113
|
+
// buffering or memory allocations. Of those characters, the ones that are
|
|
114
|
+
// non-ascii codepoints are encoded into their UTF-8 representation, of which
|
|
115
|
+
// those bytes are then compared to the C string.
|
|
116
|
+
//
|
|
117
|
+
// If the JSON type is not a JSON_STRING, then it performs a binary comparison
|
|
118
|
+
// of the raw JSON to the C string.
|
|
119
|
+
int json_string_compare(struct json json, const char *str);
|
|
120
|
+
int json_string_comparen(struct json json, const char *str, size_t len);
|
|
121
|
+
|
|
122
|
+
// json_string_length returns the number of characters needed to represent the
|
|
123
|
+
// JSON as a UTF-8 string.
|
|
124
|
+
size_t json_string_length(struct json json);
|
|
125
|
+
|
|
126
|
+
// json_string_is_escaped returns true if the json is a string that contains
|
|
127
|
+
// an escape sequence.
|
|
128
|
+
bool json_string_is_escaped(struct json json);
|
|
129
|
+
|
|
130
|
+
// json_string_copy copies a json string into the provided C string buffer.
|
|
131
|
+
//
|
|
132
|
+
// Returns the number of characters, not including the null-terminator, needed
|
|
133
|
+
// to store the JSON into the C string buffer.
|
|
134
|
+
// If the returned length is greater than nbytes-1, then only a parital copy
|
|
135
|
+
// occurred, for example:
|
|
136
|
+
//
|
|
137
|
+
// char buf[64];
|
|
138
|
+
// size_t len = json_string_copy(json, str, sizeof(str));
|
|
139
|
+
// if (len > sizeof(str)-1) {
|
|
140
|
+
// // ... copy did not complete ...
|
|
141
|
+
// }
|
|
142
|
+
//
|
|
143
|
+
size_t json_string_copy(struct json json, char *str, size_t nbytes);
|
|
144
|
+
|
|
145
|
+
// json_array_get returns the child json element at index. This is to be used
|
|
146
|
+
// on json with the type JSON_ARRAY.
|
|
147
|
+
struct json json_array_get(struct json json, size_t index);
|
|
148
|
+
|
|
149
|
+
// json_array_count returns the number of elements in a json array.
|
|
150
|
+
size_t json_array_count(struct json json);
|
|
151
|
+
|
|
152
|
+
// json_object_get returns the json value for its key. This is to be used on
|
|
153
|
+
// json with the type JSON_OBJECT.
|
|
154
|
+
struct json json_object_get(struct json json, const char *key);
|
|
155
|
+
struct json json_object_getn(struct json json, const char *key, size_t len);
|
|
156
|
+
|
|
157
|
+
// json_get finds json at the provide path.
|
|
158
|
+
//
|
|
159
|
+
// A path is a series of keys separated by a dot.
|
|
160
|
+
//
|
|
161
|
+
// json_get(json_str, "name");
|
|
162
|
+
// json_get(json_str, "user.id");
|
|
163
|
+
//
|
|
164
|
+
// The syntax is limited to very basic key names containing only alphanumeric
|
|
165
|
+
// characters, underscores, and dashes. If you need to access keys that have a
|
|
166
|
+
// broader range of characters, you can use the json_object_get and
|
|
167
|
+
// json_array_get functions.
|
|
168
|
+
//
|
|
169
|
+
// json_str = "{\"notes\": {\"special.info\": \"it's nice outside\"}";
|
|
170
|
+
// json = json_parse(json_str);
|
|
171
|
+
// json = json_object_get(json, "notes");
|
|
172
|
+
// json = json_object_get(json, "special.info");
|
|
173
|
+
//
|
|
174
|
+
struct json json_get(const char *json_str, const char *path);
|
|
175
|
+
struct json json_getn(const char *json_str, size_t len, const char *path);
|
|
176
|
+
|
|
177
|
+
// json_double returns a json's double value.
|
|
178
|
+
double json_double(struct json json);
|
|
179
|
+
|
|
180
|
+
// json_int returns a json's int value.
|
|
181
|
+
int json_int(struct json json);
|
|
182
|
+
|
|
183
|
+
// json_int64 returns a json's int64 value.
|
|
184
|
+
int64_t json_int64(struct json json);
|
|
185
|
+
|
|
186
|
+
// json_uint64 returns a json's uint64 value.
|
|
187
|
+
uint64_t json_uint64(struct json json);
|
|
188
|
+
|
|
189
|
+
// json_bool returns a json's boolean value.
|
|
190
|
+
bool json_bool(struct json json);
|
|
191
|
+
|
|
192
|
+
// json_escape is a utility function for converting a C string into a valid
|
|
193
|
+
// json string.
|
|
194
|
+
//
|
|
195
|
+
// Returns the number of characters, not including the null-terminator, needed
|
|
196
|
+
// to store the escaped JSON string in the C string buffer.
|
|
197
|
+
// If the returned length is greater than n-1, then only a parital copy
|
|
198
|
+
// occurred. In other words the
|
|
199
|
+
size_t json_escape(const char *str, char *escaped, size_t n);
|
|
200
|
+
size_t json_escapen(const char *str, size_t len, char *escaped, size_t n);
|
|
201
|
+
|
|
202
|
+
// json_ensure ensures that the json value has been entirely scanned.
|
|
203
|
+
// Only useful for when you need to call json_raw_length on large objects and
|
|
204
|
+
// arrays more than once.
|
|
205
|
+
struct json json_ensure(struct json json);
|
|
206
|
+
|
|
207
|
+
#endif // JSON_H
|
data/lib/tg/geometry/registry.rb
CHANGED
|
@@ -11,9 +11,9 @@ module TG
|
|
|
11
11
|
}.freeze
|
|
12
12
|
|
|
13
13
|
class << self
|
|
14
|
-
def source(&
|
|
15
|
-
if
|
|
16
|
-
@source =
|
|
14
|
+
def source(&definition)
|
|
15
|
+
if definition
|
|
16
|
+
@source = definition
|
|
17
17
|
elsif instance_variable_defined?(:@source)
|
|
18
18
|
@source
|
|
19
19
|
elsif superclass.respond_to?(:source)
|
data/lib/tg/geometry/version.rb
CHANGED
data/script/vendor_libs.rb
CHANGED
|
@@ -26,6 +26,14 @@ PINS = {
|
|
|
26
26
|
sha256: "4afc86cbd3abe03730206031a5aff5b8b29d37b055fc356052f6f06e1d1f9a61",
|
|
27
27
|
target: "rtree",
|
|
28
28
|
files: %w[rtree.c rtree.h LICENSE README.md]
|
|
29
|
+
},
|
|
30
|
+
json: {
|
|
31
|
+
repo: "https://github.com/tidwall/json.c.git",
|
|
32
|
+
ref: "main",
|
|
33
|
+
commit: "4aaf99b5c08e4e1364f97500fe5c5d5a2617b4a6",
|
|
34
|
+
sha256: "059393302de9325ab6ccf79c298234f2b688a59d138103305da317d2d0176fe2",
|
|
35
|
+
target: "json",
|
|
36
|
+
files: %w[json.c json.h LICENSE]
|
|
29
37
|
}
|
|
30
38
|
}.freeze
|
|
31
39
|
|
|
@@ -160,9 +168,13 @@ def write_manifest(results)
|
|
|
160
168
|
File.utime(NORMALIZED_MTIME, NORMALIZED_MTIME, MANIFEST_PATH)
|
|
161
169
|
end
|
|
162
170
|
|
|
163
|
-
def expected_vendor_results
|
|
171
|
+
def expected_vendor_results(manifest = nil)
|
|
172
|
+
manifest ||= parse_kv_file(MANIFEST_PATH)
|
|
173
|
+
|
|
164
174
|
PINS.each_with_object({}) do |(name, pin), results|
|
|
165
|
-
|
|
175
|
+
manifest_sha = manifest["#{name}_tree_sha256"]
|
|
176
|
+
tree_sha = pin.fetch(:sha256) || manifest_sha
|
|
177
|
+
results[name] = pin.merge(tree_sha256: tree_sha)
|
|
166
178
|
end
|
|
167
179
|
end
|
|
168
180
|
|
|
@@ -186,6 +198,7 @@ end
|
|
|
186
198
|
|
|
187
199
|
def verify_vendor_tree
|
|
188
200
|
failures = []
|
|
201
|
+
manifest = parse_kv_file(MANIFEST_PATH)
|
|
189
202
|
|
|
190
203
|
PINS.each do |name, pin|
|
|
191
204
|
target = File.join(VENDOR_DIR, pin.fetch(:target))
|
|
@@ -207,12 +220,15 @@ def verify_vendor_tree
|
|
|
207
220
|
next unless failures.none? { |failure| failure.start_with?("#{name}:") }
|
|
208
221
|
|
|
209
222
|
actual_sha256 = tree_sha256_for(target)
|
|
210
|
-
expected_sha256 = pin.fetch(:sha256)
|
|
211
|
-
|
|
223
|
+
expected_sha256 = pin.fetch(:sha256) || manifest["#{name}_tree_sha256"]
|
|
224
|
+
if expected_sha256.nil? || expected_sha256.empty?
|
|
225
|
+
failures << "#{name}: tree_sha256 missing from manifest"
|
|
226
|
+
elsif actual_sha256 != expected_sha256
|
|
227
|
+
failures << "#{name}: tree_sha256 mismatch: expected #{expected_sha256}, got #{actual_sha256}"
|
|
228
|
+
end
|
|
212
229
|
end
|
|
213
230
|
|
|
214
|
-
expected_results = expected_vendor_results
|
|
215
|
-
manifest = parse_kv_file(MANIFEST_PATH)
|
|
231
|
+
expected_results = expected_vendor_results(manifest)
|
|
216
232
|
if manifest.empty?
|
|
217
233
|
failures << "manifest: missing #{MANIFEST_PATH}"
|
|
218
234
|
else
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
RSpec.describe "
|
|
5
|
+
RSpec.describe "Auto strategy status" do
|
|
6
6
|
it "does not expose :auto in the first public release" do
|
|
7
7
|
entries = [[1, '{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}']]
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
RSpec.describe "
|
|
5
|
+
RSpec.describe "Packed batch API" do
|
|
6
6
|
let(:zone_a) { TG::Geometry.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") }
|
|
7
7
|
let(:zone_b) { TG::Geometry.parse_wkt("POLYGON ((20 20, 30 20, 30 30, 20 30, 20 20))") }
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
RSpec.describe "
|
|
5
|
+
RSpec.describe "Concurrency hardening" do
|
|
6
6
|
let(:zone_a) { '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}' }
|
|
7
7
|
let(:zone_b) { '{"type":"Polygon","coordinates":[[[5,5],[15,5],[15,15],[5,15],[5,5]]]}' }
|
|
8
8
|
let(:zone_c) { '{"type":"Polygon","coordinates":[[[100,100],[110,100],[110,110],[100,110],[100,100]]]}' }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
RSpec.describe "
|
|
5
|
+
RSpec.describe "Error hardening" do
|
|
6
6
|
let(:geojson) { '{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,1],[0,0]]]}' }
|
|
7
7
|
let(:wkb) { TG::Geometry.parse_geojson(geojson).to_wkb }
|
|
8
8
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "FeatureSource no-GVL behavior" do
|
|
7
|
+
def polygon_feature_json(id, x, y)
|
|
8
|
+
<<~JSON.chomp
|
|
9
|
+
{"type":"Feature","properties":{"@id":"zone/#{id}"},"geometry":{"type":"Polygon","coordinates":[[[#{x},#{y}],[#{x + 0.8},#{y}],[#{x + 0.8},#{y + 0.8}],[#{x},#{y + 0.8}],[#{x},#{y}]]]}}
|
|
10
|
+
JSON
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def write_large_feature_collection(count)
|
|
14
|
+
file = Tempfile.new(["tg_geometry_feature_source_nogvl", ".geojson"])
|
|
15
|
+
file.binmode
|
|
16
|
+
file.write('{"type":"FeatureCollection","features":[')
|
|
17
|
+
count.times do |i|
|
|
18
|
+
file.write(",") unless i.zero?
|
|
19
|
+
file.write(polygon_feature_json(i, i % 1000, i / 1000))
|
|
20
|
+
end
|
|
21
|
+
file.write("]}")
|
|
22
|
+
file.flush
|
|
23
|
+
file.close
|
|
24
|
+
file
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "lets other Ruby threads run during large FeatureSource file processing" do
|
|
28
|
+
count = 40_000
|
|
29
|
+
file = write_large_feature_collection(count)
|
|
30
|
+
ticks = 0
|
|
31
|
+
done = false
|
|
32
|
+
|
|
33
|
+
ticker = Thread.new do
|
|
34
|
+
until done
|
|
35
|
+
ticks += 1
|
|
36
|
+
sleep 0.005
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
index = TG::Geometry::FeatureSource.build_index_file(file.path, strategy: :flat)
|
|
41
|
+
done = true
|
|
42
|
+
ticker.join
|
|
43
|
+
|
|
44
|
+
expect(index.size).to eq(count)
|
|
45
|
+
expect(ticks).to be >= 1
|
|
46
|
+
ensure
|
|
47
|
+
done = true
|
|
48
|
+
ticker&.join
|
|
49
|
+
file&.unlink
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "FeatureSource" do
|
|
7
|
+
let(:fixture_dir) { File.join(__dir__, "fixtures", "feature_source") }
|
|
8
|
+
let(:simple_path) { File.join(fixture_dir, "simple_feature_collection.geojson") }
|
|
9
|
+
let(:simple_json) { File.binread(simple_path) }
|
|
10
|
+
|
|
11
|
+
it "defines TG::Geometry::FeatureSource" do
|
|
12
|
+
expect(TG::Geometry.const_defined?(:FeatureSource)).to be(true)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "reads entries from JSON, file, and IO without parsing properties to Hash" do
|
|
16
|
+
expected = [["zone/a", /"Polygon"/], [2, /"MultiPolygon"/]]
|
|
17
|
+
|
|
18
|
+
[
|
|
19
|
+
TG::Geometry::FeatureSource.read_entries_json(simple_json),
|
|
20
|
+
TG::Geometry::FeatureSource.read_entries_file(simple_path),
|
|
21
|
+
TG::Geometry::FeatureSource.read_entries_io(StringIO.new(simple_json))
|
|
22
|
+
].each do |entries|
|
|
23
|
+
expect(entries.size).to eq(2)
|
|
24
|
+
expected.each_with_index do |(id, geometry_matcher), i|
|
|
25
|
+
expect(entries[i][0]).to eq(id)
|
|
26
|
+
expect(entries[i][1]).to match(geometry_matcher)
|
|
27
|
+
expect(entries[i][1]).to be_a(String)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "reads features with raw properties JSON" do
|
|
33
|
+
features = TG::Geometry::FeatureSource.read_features_json(simple_json)
|
|
34
|
+
|
|
35
|
+
expect(features.size).to eq(2)
|
|
36
|
+
expect(features[0][0]).to eq("zone/a")
|
|
37
|
+
expect(features[0][1]).to include('"Polygon"')
|
|
38
|
+
expect(features[0][2]).to include('"@id": "zone/a"')
|
|
39
|
+
expect(features[1][2]).to include('"@id": 2')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "uses the OSM-style default id path [properties, @id]" do
|
|
43
|
+
json = File.binread(File.join(fixture_dir, "osm_like_feature_collection.geojson"))
|
|
44
|
+
|
|
45
|
+
entries = TG::Geometry::FeatureSource.read_entries_json(json)
|
|
46
|
+
|
|
47
|
+
expect(entries.first.first).to eq("relation/100")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "filters non polygon types by default and reports filtered separately" do
|
|
51
|
+
json = File.binread(File.join(fixture_dir, "mixed_geometry_types.geojson"))
|
|
52
|
+
|
|
53
|
+
report = TG::Geometry::FeatureSource.read_entries_json(json, report: true)
|
|
54
|
+
|
|
55
|
+
expect(report[:entries].map(&:first)).to eq(["poly"])
|
|
56
|
+
expect(report[:filtered]).to eq(2)
|
|
57
|
+
expect(report[:skipped]).to eq(0)
|
|
58
|
+
expect(report[:errors]).to eq([])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "supports only: nil for no geometry type filtering" do
|
|
62
|
+
json = File.binread(File.join(fixture_dir, "mixed_geometry_types.geojson"))
|
|
63
|
+
|
|
64
|
+
entries = TG::Geometry::FeatureSource.read_entries_json(json, only: nil)
|
|
65
|
+
|
|
66
|
+
expect(entries.map(&:first)).to eq(%w[poly point line])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "requires report mode for skip options" do
|
|
70
|
+
expect do
|
|
71
|
+
TG::Geometry::FeatureSource.read_entries_json(simple_json, on_invalid: :skip)
|
|
72
|
+
end.to raise_error(TG::Geometry::ArgumentError)
|
|
73
|
+
|
|
74
|
+
expect do
|
|
75
|
+
TG::Geometry::FeatureSource.read_entries_json(simple_json, on_missing_id: :skip)
|
|
76
|
+
end.to raise_error(TG::Geometry::ArgumentError)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "supports on_missing_id: :ordinal" do
|
|
80
|
+
json = File.binread(File.join(fixture_dir, "properties_null_missing.geojson"))
|
|
81
|
+
|
|
82
|
+
entries = TG::Geometry::FeatureSource.read_entries_json(json, on_missing_id: :ordinal)
|
|
83
|
+
|
|
84
|
+
expect(entries.map(&:first)).to eq(["feature/0", "feature/1"])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "supports on_missing_id: :skip in report mode without requiring on_invalid: :skip" do
|
|
88
|
+
json = File.binread(File.join(fixture_dir, "properties_null_missing.geojson"))
|
|
89
|
+
|
|
90
|
+
report = TG::Geometry::FeatureSource.read_entries_json(json,
|
|
91
|
+
report: true,
|
|
92
|
+
on_missing_id: :skip)
|
|
93
|
+
|
|
94
|
+
expect(report[:entries]).to eq([])
|
|
95
|
+
expect(report[:skipped]).to eq(2)
|
|
96
|
+
expect(report[:filtered]).to eq(0)
|
|
97
|
+
expect(report[:errors].map { |e| e[:feature_index] }).to eq([0, 1])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "reports invalid geometry with capped errors and exact skipped count" do
|
|
101
|
+
json = File.binread(File.join(fixture_dir, "invalid_geometry_middle.geojson"))
|
|
102
|
+
|
|
103
|
+
report = TG::Geometry::FeatureSource.read_entries_json(json,
|
|
104
|
+
report: true,
|
|
105
|
+
on_invalid: :skip,
|
|
106
|
+
max_errors: 1)
|
|
107
|
+
|
|
108
|
+
expect(report[:entries].map(&:first)).to eq(["ok-a", "ok-b"])
|
|
109
|
+
expect(report[:skipped]).to eq(1)
|
|
110
|
+
expect(report[:errors].size).to eq(1)
|
|
111
|
+
expect(report[:errors].first[:feature_index]).to eq(1)
|
|
112
|
+
expect(report[:errors].first[:reason]).to include("invalid geometry")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "raises ParseError for malformed JSON and non FeatureCollection roots" do
|
|
116
|
+
malformed = File.binread(File.join(fixture_dir, "malformed_json.geojson"))
|
|
117
|
+
|
|
118
|
+
expect { TG::Geometry::FeatureSource.read_entries_json(malformed) }
|
|
119
|
+
.to raise_error(TG::Geometry::ParseError)
|
|
120
|
+
expect { TG::Geometry::FeatureSource.read_entries_json('{"type":"Feature"}') }
|
|
121
|
+
.to raise_error(TG::Geometry::ParseError)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "builds an Index directly from JSON, file, and IO" do
|
|
125
|
+
indexes = [
|
|
126
|
+
TG::Geometry::FeatureSource.build_index_json(simple_json, strategy: :flat),
|
|
127
|
+
TG::Geometry::FeatureSource.build_index_file(simple_path, strategy: :flat),
|
|
128
|
+
TG::Geometry::FeatureSource.build_index_io(StringIO.new(simple_json), strategy: :flat)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
indexes.each do |index|
|
|
132
|
+
expect(index).to be_a(TG::Geometry::Index)
|
|
133
|
+
expect(index).to be_frozen
|
|
134
|
+
expect(index.size).to eq(2)
|
|
135
|
+
expect(index.find_covering(5, 5)).to eq("zone/a")
|
|
136
|
+
expect(index.find_covering(25, 25)).to eq(2)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "matches Index.build(read_entries, via: :geojson) behavior" do
|
|
141
|
+
entries = TG::Geometry::FeatureSource.read_entries_json(simple_json)
|
|
142
|
+
from_entries = TG::Geometry::Index.build(entries, via: :geojson, strategy: :flat)
|
|
143
|
+
direct = TG::Geometry::FeatureSource.build_index_json(simple_json, strategy: :flat)
|
|
144
|
+
|
|
145
|
+
[[5, 5], [25, 25], [100, 100]].each do |lon, lat|
|
|
146
|
+
expect(direct.find_covering(lon, lat)).to eq(from_entries.find_covering(lon, lat))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
it "raises ParseError with a string reason for missing geometry" do
|
|
152
|
+
json = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"@id":"x"}}]}'
|
|
153
|
+
|
|
154
|
+
expect { TG::Geometry::FeatureSource.read_entries_json(json) }
|
|
155
|
+
.to raise_error(TG::Geometry::ParseError, /feature 0.*missing geometry/)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "reports missing geometry with a string reason in skip mode" do
|
|
159
|
+
json = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"@id":"x"}}]}'
|
|
160
|
+
|
|
161
|
+
report = TG::Geometry::FeatureSource.read_entries_json(
|
|
162
|
+
json,
|
|
163
|
+
report: true,
|
|
164
|
+
on_invalid: :skip
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
expect(report[:entries]).to eq([])
|
|
168
|
+
expect(report[:skipped]).to eq(1)
|
|
169
|
+
expect(report[:filtered]).to eq(0)
|
|
170
|
+
expect(report[:errors].first[:reason]).to be_a(String)
|
|
171
|
+
expect(report[:errors].first[:reason]).to include("missing geometry")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "skips invalid id type when on_invalid is skip and report is true" do
|
|
175
|
+
json = <<~JSON
|
|
176
|
+
{"type":"FeatureCollection","features":[
|
|
177
|
+
{"type":"Feature","properties":{"@id":false},"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}}
|
|
178
|
+
]}
|
|
179
|
+
JSON
|
|
180
|
+
|
|
181
|
+
report = TG::Geometry::FeatureSource.read_entries_json(
|
|
182
|
+
json,
|
|
183
|
+
report: true,
|
|
184
|
+
on_invalid: :skip
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
expect(report[:entries]).to eq([])
|
|
188
|
+
expect(report[:skipped]).to eq(1)
|
|
189
|
+
expect(report[:filtered]).to eq(0)
|
|
190
|
+
expect(report[:errors].first[:reason]).to include("invalid id")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "skips fractional numeric id when on_invalid is skip and report is true" do
|
|
194
|
+
json = <<~JSON
|
|
195
|
+
{"type":"FeatureCollection","features":[
|
|
196
|
+
{"type":"Feature","properties":{"@id":1.2},"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}}
|
|
197
|
+
]}
|
|
198
|
+
JSON
|
|
199
|
+
|
|
200
|
+
report = TG::Geometry::FeatureSource.read_entries_json(
|
|
201
|
+
json,
|
|
202
|
+
report: true,
|
|
203
|
+
on_invalid: :skip
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
expect(report[:entries]).to eq([])
|
|
207
|
+
expect(report[:skipped]).to eq(1)
|
|
208
|
+
expect(report[:errors].first[:reason]).to include("numeric id must be an integer")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "raises ParseError for malformed feature shapes" do
|
|
212
|
+
cases = [
|
|
213
|
+
'[1]',
|
|
214
|
+
'{"type":"Feature","properties":{"@id":"x"},"geometry":null}',
|
|
215
|
+
'{"type":"Feature","properties":{"@id":"x"},"geometry":"bad"}',
|
|
216
|
+
'{"type":"Feature","properties":{"@id":"x"},"geometry":{"coordinates":[]}}',
|
|
217
|
+
'{"type":"Feature","properties":{"@id":"x"},"geometry":{"type":1,"coordinates":[]}}'
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
cases.each do |feature_json|
|
|
221
|
+
json = "{\"type\":\"FeatureCollection\",\"features\":[#{feature_json}]}"
|
|
222
|
+
expect { TG::Geometry::FeatureSource.read_entries_json(json) }
|
|
223
|
+
.to raise_error(TG::Geometry::ParseError)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "raises ParseError for missing or non-array features" do
|
|
228
|
+
expect { TG::Geometry::FeatureSource.read_entries_json('{"type":"FeatureCollection"}') }
|
|
229
|
+
.to raise_error(TG::Geometry::ParseError, /features/)
|
|
230
|
+
|
|
231
|
+
expect { TG::Geometry::FeatureSource.read_entries_json('{"type":"FeatureCollection","features":{}}') }
|
|
232
|
+
.to raise_error(TG::Geometry::ParseError, /features/)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "treats invalid properties type as invalid feature for read_features" do
|
|
236
|
+
json = <<~JSON
|
|
237
|
+
{"type":"FeatureCollection","features":[
|
|
238
|
+
{"type":"Feature","properties":[],"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}}
|
|
239
|
+
]}
|
|
240
|
+
JSON
|
|
241
|
+
|
|
242
|
+
expect { TG::Geometry::FeatureSource.read_features_json(json, on_missing_id: :ordinal) }
|
|
243
|
+
.to raise_error(TG::Geometry::ParseError, /properties must be an object or null/)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it "rejects unknown keywords" do
|
|
247
|
+
expect { TG::Geometry::FeatureSource.read_entries_json(simple_json, typo: 1) }
|
|
248
|
+
.to raise_error(TG::Geometry::ArgumentError, /unknown keyword/)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "surfaces missing file errors as file errors" do
|
|
252
|
+
missing = File.join(fixture_dir, "does_not_exist.geojson")
|
|
253
|
+
|
|
254
|
+
expect { TG::Geometry::FeatureSource.read_entries_file(missing) }
|
|
255
|
+
.to raise_error(Errno::ENOENT)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it "rejects invalid id types" do
|
|
259
|
+
json = <<~JSON
|
|
260
|
+
{"type":"FeatureCollection","features":[
|
|
261
|
+
{"type":"Feature","properties":{"@id":false},"geometry":{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}}
|
|
262
|
+
]}
|
|
263
|
+
JSON
|
|
264
|
+
|
|
265
|
+
expect { TG::Geometry::FeatureSource.read_entries_json(json) }
|
|
266
|
+
.to raise_error(TG::Geometry::ArgumentError)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
|
-
RSpec.describe "
|
|
6
|
+
RSpec.describe "Fuzz hardening" do
|
|
7
7
|
ITERATIONS = Integer(ENV.fetch("TG_GEOMETRY_FUZZ_ITERATIONS", "2000")).freeze
|
|
8
8
|
SEED = Integer(ENV.fetch("TG_GEOMETRY_FUZZ_SEED", "20260524")).freeze
|
|
9
9
|
CORPUS_DIR = File.expand_path("fixtures/fuzz_corpus", __dir__).freeze
|