jq 1.0.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 +7 -0
- data/.github/workflows/ci.yml +32 -0
- data/.github/workflows/release-gem.yml +38 -0
- data/.gitignore +49 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +21 -0
- data/README.md +214 -0
- data/Rakefile +15 -0
- data/SECURITY.md +121 -0
- data/ext/jq/extconf.rb +79 -0
- data/ext/jq/jq_ext.c +385 -0
- data/ext/jq/jq_ext.h +24 -0
- data/jq.gemspec +35 -0
- data/lib/jq.rb +101 -0
- data/sig/jq.rbs +48 -0
- metadata +123 -0
data/ext/jq/jq_ext.c
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/* frozen_string_literal: true */
|
|
2
|
+
|
|
3
|
+
#include "jq_ext.h"
|
|
4
|
+
#include <string.h>
|
|
5
|
+
|
|
6
|
+
// Global variables for Ruby module and exception classes
|
|
7
|
+
VALUE rb_mJQ;
|
|
8
|
+
VALUE rb_eJQError;
|
|
9
|
+
VALUE rb_eJQCompileError;
|
|
10
|
+
VALUE rb_eJQRuntimeError;
|
|
11
|
+
VALUE rb_eJQParseError;
|
|
12
|
+
|
|
13
|
+
// Forward declarations for static helper functions
|
|
14
|
+
static VALUE jv_to_json_string(jv value, int raw, int compact, int sort);
|
|
15
|
+
static void raise_jq_error(jv error_value, VALUE exception_class);
|
|
16
|
+
static VALUE rb_jq_filter_impl(const char *json_str, const char *filter_str,
|
|
17
|
+
int raw_output, int compact_output,
|
|
18
|
+
int sort_keys, int multiple_outputs);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert a jv value to a Ruby JSON string
|
|
22
|
+
*
|
|
23
|
+
* @param value The jv value to convert (CONSUMED by this function)
|
|
24
|
+
* @param raw If true and value is string, return raw string (jq -r)
|
|
25
|
+
* @param compact If true, use compact JSON output (jq -c)
|
|
26
|
+
* @param sort If true, sort object keys (jq -S)
|
|
27
|
+
* @return Ruby string containing JSON or raw value
|
|
28
|
+
*/
|
|
29
|
+
static VALUE jv_to_json_string(jv value, int raw, int compact, int sort) {
|
|
30
|
+
int flags = 0;
|
|
31
|
+
// Compact is the default; JV_PRINT_PRETTY makes it non-compact
|
|
32
|
+
if (!compact) flags |= JV_PRINT_PRETTY;
|
|
33
|
+
if (sort) flags |= JV_PRINT_SORTED;
|
|
34
|
+
|
|
35
|
+
jv json;
|
|
36
|
+
VALUE result;
|
|
37
|
+
|
|
38
|
+
// Raw output - return string directly without JSON encoding
|
|
39
|
+
if (raw && jv_get_kind(value) == JV_KIND_STRING) {
|
|
40
|
+
const char *str = jv_string_value(value);
|
|
41
|
+
size_t len = jv_string_length_bytes(jv_copy(value));
|
|
42
|
+
result = rb_utf8_str_new(str, len);
|
|
43
|
+
jv_free(value); // Free the string value
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Convert to JSON string
|
|
48
|
+
json = jv_dump_string(value, flags); // CONSUMES value
|
|
49
|
+
|
|
50
|
+
if (!jv_is_valid(json)) {
|
|
51
|
+
jv_free(json);
|
|
52
|
+
rb_raise(rb_eJQRuntimeError, "Failed to convert result to JSON");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const char *json_str = jv_string_value(json);
|
|
56
|
+
size_t json_len = jv_string_length_bytes(jv_copy(json));
|
|
57
|
+
result = rb_utf8_str_new(json_str, json_len);
|
|
58
|
+
jv_free(json); // Free the JSON string
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Raise a Ruby exception from a jv error value
|
|
65
|
+
*
|
|
66
|
+
* @param error_value The jv error message (CONSUMED by this function)
|
|
67
|
+
* @param exception_class The Ruby exception class to raise
|
|
68
|
+
*/
|
|
69
|
+
static void raise_jq_error(jv error_value, VALUE exception_class) {
|
|
70
|
+
if (!jv_is_valid(error_value) ||
|
|
71
|
+
jv_get_kind(error_value) != JV_KIND_STRING) {
|
|
72
|
+
jv_free(error_value);
|
|
73
|
+
rb_raise(exception_class, "Unknown jq error");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const char *msg = jv_string_value(error_value);
|
|
77
|
+
VALUE rb_msg = rb_str_new_cstr(msg);
|
|
78
|
+
jv_free(error_value); // Free the error message
|
|
79
|
+
|
|
80
|
+
// Store C string before rb_raise (StringValueCStr can raise if encoding issues occur)
|
|
81
|
+
const char *msg_cstr = StringValueCStr(rb_msg);
|
|
82
|
+
rb_raise(exception_class, "%s", msg_cstr);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Implementation of JQ.filter
|
|
87
|
+
*
|
|
88
|
+
* @param json_str JSON input string
|
|
89
|
+
* @param filter_str jq filter expression
|
|
90
|
+
* @param raw_output If true, output raw strings (jq -r)
|
|
91
|
+
* @param compact_output If true, output compact JSON (jq -c)
|
|
92
|
+
* @param sort_keys If true, sort object keys (jq -S)
|
|
93
|
+
* @param multiple_outputs If true, return array of all results
|
|
94
|
+
* @return Ruby string or array of strings
|
|
95
|
+
*/
|
|
96
|
+
static VALUE rb_jq_filter_impl(const char *json_str, const char *filter_str,
|
|
97
|
+
int raw_output, int compact_output,
|
|
98
|
+
int sort_keys, int multiple_outputs) {
|
|
99
|
+
jq_state *jq = NULL;
|
|
100
|
+
jv input = jv_invalid();
|
|
101
|
+
VALUE results = Qnil;
|
|
102
|
+
jv result;
|
|
103
|
+
|
|
104
|
+
// Initialize jq
|
|
105
|
+
jq = jq_init();
|
|
106
|
+
if (!jq) {
|
|
107
|
+
rb_raise(rb_eJQError, "Failed to initialize jq");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Compile filter
|
|
111
|
+
if (!jq_compile(jq, filter_str)) {
|
|
112
|
+
jv error = jq_get_error_message(jq);
|
|
113
|
+
|
|
114
|
+
if (jv_is_valid(error) && jv_get_kind(error) == JV_KIND_STRING) {
|
|
115
|
+
const char *error_msg = jv_string_value(error);
|
|
116
|
+
VALUE rb_error_msg = rb_str_new_cstr(error_msg);
|
|
117
|
+
jv_free(error);
|
|
118
|
+
// Store C string before cleanup (StringValueCStr can raise)
|
|
119
|
+
const char *error_cstr = StringValueCStr(rb_error_msg);
|
|
120
|
+
jq_teardown(&jq);
|
|
121
|
+
rb_raise(rb_eJQCompileError, "%s", error_cstr);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
jv_free(error);
|
|
125
|
+
jq_teardown(&jq);
|
|
126
|
+
rb_raise(rb_eJQCompileError, "Syntax error in jq filter");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Parse JSON input
|
|
130
|
+
input = jv_parse(json_str);
|
|
131
|
+
if (!jv_is_valid(input)) {
|
|
132
|
+
if (jv_invalid_has_msg(jv_copy(input))) {
|
|
133
|
+
jv error_msg = jv_invalid_get_msg(input); // CONSUMES input
|
|
134
|
+
jq_teardown(&jq);
|
|
135
|
+
raise_jq_error(error_msg, rb_eJQParseError);
|
|
136
|
+
}
|
|
137
|
+
jv_free(input);
|
|
138
|
+
jq_teardown(&jq);
|
|
139
|
+
rb_raise(rb_eJQParseError, "Invalid JSON input");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Process with jq
|
|
143
|
+
jq_start(jq, input, 0); // CONSUMES input
|
|
144
|
+
|
|
145
|
+
// Collect results
|
|
146
|
+
if (multiple_outputs) {
|
|
147
|
+
results = rb_ary_new();
|
|
148
|
+
|
|
149
|
+
while (jv_is_valid(result = jq_next(jq))) {
|
|
150
|
+
VALUE json = jv_to_json_string(result, raw_output,
|
|
151
|
+
compact_output, sort_keys);
|
|
152
|
+
rb_ary_push(results, json);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if the final invalid result has an error message
|
|
156
|
+
if (jv_invalid_has_msg(jv_copy(result))) {
|
|
157
|
+
jv error_msg = jv_invalid_get_msg(result); // CONSUMES result
|
|
158
|
+
jq_teardown(&jq);
|
|
159
|
+
raise_jq_error(error_msg, rb_eJQRuntimeError);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
jv_free(result); // Free the invalid/end marker
|
|
163
|
+
} else {
|
|
164
|
+
result = jq_next(jq);
|
|
165
|
+
|
|
166
|
+
if (jv_is_valid(result)) {
|
|
167
|
+
results = jv_to_json_string(result, raw_output,
|
|
168
|
+
compact_output, sort_keys);
|
|
169
|
+
} else if (jv_invalid_has_msg(jv_copy(result))) {
|
|
170
|
+
jv error_msg = jv_invalid_get_msg(result); // CONSUMES result
|
|
171
|
+
jq_teardown(&jq);
|
|
172
|
+
raise_jq_error(error_msg, rb_eJQRuntimeError);
|
|
173
|
+
} else {
|
|
174
|
+
jv_free(result);
|
|
175
|
+
// No results - return null
|
|
176
|
+
results = rb_str_new_cstr("null");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
jq_teardown(&jq);
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* call-seq:
|
|
186
|
+
* JQ.filter(json, filter, **options) -> String or Array<String>
|
|
187
|
+
*
|
|
188
|
+
* Apply a jq filter to JSON input and return the result.
|
|
189
|
+
*
|
|
190
|
+
* This is the primary method for using jq from Ruby. It parses the JSON input,
|
|
191
|
+
* compiles the filter expression, executes it, and returns the result as a
|
|
192
|
+
* JSON string (or array of strings with +multiple_outputs: true+).
|
|
193
|
+
*
|
|
194
|
+
* === Parameters
|
|
195
|
+
*
|
|
196
|
+
* [json (String)] Valid JSON input string
|
|
197
|
+
* [filter (String)] jq filter expression (e.g., ".name", ".[] | select(.age > 18)")
|
|
198
|
+
*
|
|
199
|
+
* === Options
|
|
200
|
+
*
|
|
201
|
+
* [:raw_output (Boolean)] Return raw strings without JSON encoding (equivalent to jq -r). Default: false
|
|
202
|
+
* [:compact_output (Boolean)] Output compact JSON on a single line. Default: true (set to false for pretty output)
|
|
203
|
+
* [:sort_keys (Boolean)] Sort object keys alphabetically (equivalent to jq -S). Default: false
|
|
204
|
+
* [:multiple_outputs (Boolean)] Return array of all results instead of just the first. Default: false
|
|
205
|
+
*
|
|
206
|
+
* === Returns
|
|
207
|
+
*
|
|
208
|
+
* [String] JSON-encoded result (default), or raw string if +raw_output: true+
|
|
209
|
+
* [Array<String>] Array of results if +multiple_outputs: true+
|
|
210
|
+
*
|
|
211
|
+
* === Raises
|
|
212
|
+
*
|
|
213
|
+
* [JQ::ParseError] If the JSON input is invalid
|
|
214
|
+
* [JQ::CompileError] If the jq filter expression is invalid
|
|
215
|
+
* [JQ::RuntimeError] If the filter execution fails
|
|
216
|
+
* [TypeError] If arguments are not strings
|
|
217
|
+
*
|
|
218
|
+
* === Examples
|
|
219
|
+
*
|
|
220
|
+
* # Basic filtering
|
|
221
|
+
* JQ.filter('{"name":"Alice","age":30}', '.name')
|
|
222
|
+
* # => "\"Alice\""
|
|
223
|
+
*
|
|
224
|
+
* # Raw output (no JSON encoding)
|
|
225
|
+
* JQ.filter('{"name":"Alice"}', '.name', raw_output: true)
|
|
226
|
+
* # => "Alice"
|
|
227
|
+
*
|
|
228
|
+
* # Pretty output
|
|
229
|
+
* JQ.filter('{"a":1,"b":2}', '.', compact_output: false)
|
|
230
|
+
* # => "{\n \"a\": 1,\n \"b\": 2\n}"
|
|
231
|
+
*
|
|
232
|
+
* # Multiple outputs
|
|
233
|
+
* JQ.filter('[1,2,3]', '.[]', multiple_outputs: true)
|
|
234
|
+
* # => ["1", "2", "3"]
|
|
235
|
+
*
|
|
236
|
+
* # Sort keys
|
|
237
|
+
* JQ.filter('{"z":1,"a":2}', '.', sort_keys: true)
|
|
238
|
+
* # => "{\"a\":2,\"z\":1}"
|
|
239
|
+
*
|
|
240
|
+
* # Complex transformation
|
|
241
|
+
* json = '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'
|
|
242
|
+
* JQ.filter(json, '[.[] | select(.age > 26) | .name]')
|
|
243
|
+
* # => "[\"Alice\"]"
|
|
244
|
+
*
|
|
245
|
+
* === Thread Safety
|
|
246
|
+
*
|
|
247
|
+
* This method is thread-safe with jq 1.7+ (required by this gem). Each call
|
|
248
|
+
* creates an isolated jq_state, so concurrent calls do not interfere with
|
|
249
|
+
* each other.
|
|
250
|
+
*
|
|
251
|
+
*/
|
|
252
|
+
VALUE rb_jq_filter(int argc, VALUE *argv, VALUE self) {
|
|
253
|
+
VALUE json_str, filter_str, opts;
|
|
254
|
+
rb_scan_args(argc, argv, "2:", &json_str, &filter_str, &opts);
|
|
255
|
+
|
|
256
|
+
Check_Type(json_str, T_STRING);
|
|
257
|
+
Check_Type(filter_str, T_STRING);
|
|
258
|
+
|
|
259
|
+
const char *json_cstr = StringValueCStr(json_str);
|
|
260
|
+
const char *filter_cstr = StringValueCStr(filter_str);
|
|
261
|
+
|
|
262
|
+
// Parse options (default to compact output)
|
|
263
|
+
int raw_output = 0, compact_output = 1;
|
|
264
|
+
int sort_keys = 0, multiple_outputs = 0;
|
|
265
|
+
|
|
266
|
+
if (!NIL_P(opts)) {
|
|
267
|
+
Check_Type(opts, T_HASH);
|
|
268
|
+
VALUE opt;
|
|
269
|
+
|
|
270
|
+
opt = rb_hash_aref(opts, ID2SYM(rb_intern("raw_output")));
|
|
271
|
+
if (RTEST(opt)) raw_output = 1;
|
|
272
|
+
|
|
273
|
+
opt = rb_hash_aref(opts, ID2SYM(rb_intern("compact_output")));
|
|
274
|
+
if (!NIL_P(opt)) compact_output = RTEST(opt) ? 1 : 0;
|
|
275
|
+
|
|
276
|
+
opt = rb_hash_aref(opts, ID2SYM(rb_intern("sort_keys")));
|
|
277
|
+
if (RTEST(opt)) sort_keys = 1;
|
|
278
|
+
|
|
279
|
+
opt = rb_hash_aref(opts, ID2SYM(rb_intern("multiple_outputs")));
|
|
280
|
+
if (RTEST(opt)) multiple_outputs = 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return rb_jq_filter_impl(json_cstr, filter_cstr,
|
|
284
|
+
raw_output, compact_output,
|
|
285
|
+
sort_keys, multiple_outputs);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/*
|
|
289
|
+
* call-seq:
|
|
290
|
+
* JQ.validate_filter!(filter) -> true
|
|
291
|
+
*
|
|
292
|
+
* Validate a jq filter expression without executing it.
|
|
293
|
+
*
|
|
294
|
+
* This method compiles the filter to check for syntax errors without requiring
|
|
295
|
+
* any JSON input. Use this to validate user-provided filters before attempting
|
|
296
|
+
* to apply them to data.
|
|
297
|
+
*
|
|
298
|
+
* === Parameters
|
|
299
|
+
*
|
|
300
|
+
* [filter (String)] jq filter expression to validate
|
|
301
|
+
*
|
|
302
|
+
* === Returns
|
|
303
|
+
*
|
|
304
|
+
* [true] Always returns true if the filter is valid
|
|
305
|
+
*
|
|
306
|
+
* === Raises
|
|
307
|
+
*
|
|
308
|
+
* [JQ::CompileError] If the filter expression is invalid
|
|
309
|
+
* [TypeError] If filter is not a string
|
|
310
|
+
*
|
|
311
|
+
* === Examples
|
|
312
|
+
*
|
|
313
|
+
* # Valid filters return true
|
|
314
|
+
* JQ.validate_filter!('.name')
|
|
315
|
+
* # => true
|
|
316
|
+
*
|
|
317
|
+
* JQ.validate_filter!('.[] | select(.age > 18)')
|
|
318
|
+
* # => true
|
|
319
|
+
*
|
|
320
|
+
* # Invalid filters raise CompileError
|
|
321
|
+
* JQ.validate_filter!('. @@@ .')
|
|
322
|
+
* # raises JQ::CompileError: Syntax error in jq filter
|
|
323
|
+
*
|
|
324
|
+
* # Validate user input before use
|
|
325
|
+
* user_filter = params[:filter]
|
|
326
|
+
* begin
|
|
327
|
+
* JQ.validate_filter!(user_filter)
|
|
328
|
+
* result = JQ.filter(json, user_filter)
|
|
329
|
+
* rescue JQ::CompileError => e
|
|
330
|
+
* puts "Invalid filter: #{e.message}"
|
|
331
|
+
* end
|
|
332
|
+
*
|
|
333
|
+
* === Thread Safety
|
|
334
|
+
*
|
|
335
|
+
* This method is thread-safe with jq 1.7+ (required by this gem).
|
|
336
|
+
*
|
|
337
|
+
*/
|
|
338
|
+
VALUE rb_jq_validate_filter(VALUE self, VALUE filter) {
|
|
339
|
+
Check_Type(filter, T_STRING);
|
|
340
|
+
const char *filter_cstr = StringValueCStr(filter);
|
|
341
|
+
|
|
342
|
+
jq_state *jq = jq_init();
|
|
343
|
+
if (!jq) {
|
|
344
|
+
rb_raise(rb_eJQError, "Failed to initialize jq");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!jq_compile(jq, filter_cstr)) {
|
|
348
|
+
jv error = jq_get_error_message(jq);
|
|
349
|
+
|
|
350
|
+
if (jv_is_valid(error) && jv_get_kind(error) == JV_KIND_STRING) {
|
|
351
|
+
const char *error_msg = jv_string_value(error);
|
|
352
|
+
VALUE rb_error_msg = rb_str_new_cstr(error_msg);
|
|
353
|
+
jv_free(error);
|
|
354
|
+
// Store C string before cleanup (StringValueCStr can raise)
|
|
355
|
+
const char *error_cstr = StringValueCStr(rb_error_msg);
|
|
356
|
+
jq_teardown(&jq);
|
|
357
|
+
rb_raise(rb_eJQCompileError, "%s", error_cstr);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
jv_free(error);
|
|
361
|
+
jq_teardown(&jq);
|
|
362
|
+
rb_raise(rb_eJQCompileError, "Syntax error in jq filter");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
jq_teardown(&jq);
|
|
366
|
+
return Qtrue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Initialize the jq extension
|
|
371
|
+
*/
|
|
372
|
+
void Init_jq_ext(void) {
|
|
373
|
+
// Define module
|
|
374
|
+
rb_mJQ = rb_define_module("JQ");
|
|
375
|
+
|
|
376
|
+
// Define exception classes
|
|
377
|
+
rb_eJQError = rb_define_class_under(rb_mJQ, "Error", rb_eStandardError);
|
|
378
|
+
rb_eJQCompileError = rb_define_class_under(rb_mJQ, "CompileError", rb_eJQError);
|
|
379
|
+
rb_eJQRuntimeError = rb_define_class_under(rb_mJQ, "RuntimeError", rb_eJQError);
|
|
380
|
+
rb_eJQParseError = rb_define_class_under(rb_mJQ, "ParseError", rb_eJQError);
|
|
381
|
+
|
|
382
|
+
// Define methods
|
|
383
|
+
rb_define_singleton_method(rb_mJQ, "filter", rb_jq_filter, -1);
|
|
384
|
+
rb_define_singleton_method(rb_mJQ, "validate_filter!", rb_jq_validate_filter, 1);
|
|
385
|
+
}
|
data/ext/jq/jq_ext.h
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* frozen_string_literal: true */
|
|
2
|
+
|
|
3
|
+
#ifndef JQ_EXT_H
|
|
4
|
+
#define JQ_EXT_H
|
|
5
|
+
|
|
6
|
+
#include <ruby.h>
|
|
7
|
+
#include <jq.h>
|
|
8
|
+
#include <jv.h>
|
|
9
|
+
|
|
10
|
+
// Ruby module and exception classes
|
|
11
|
+
extern VALUE rb_mJQ;
|
|
12
|
+
extern VALUE rb_eJQError;
|
|
13
|
+
extern VALUE rb_eJQCompileError;
|
|
14
|
+
extern VALUE rb_eJQRuntimeError;
|
|
15
|
+
extern VALUE rb_eJQParseError;
|
|
16
|
+
|
|
17
|
+
// Main methods
|
|
18
|
+
VALUE rb_jq_filter(int argc, VALUE *argv, VALUE self);
|
|
19
|
+
VALUE rb_jq_validate_filter(VALUE self, VALUE filter);
|
|
20
|
+
|
|
21
|
+
// Initialization
|
|
22
|
+
void Init_jq_ext(void);
|
|
23
|
+
|
|
24
|
+
#endif /* JQ_EXT_H */
|
data/jq.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "jq"
|
|
5
|
+
spec.version = "1.0.0"
|
|
6
|
+
spec.authors = ["Persona Identities"]
|
|
7
|
+
spec.email = ["rubygems@withpersona.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Ruby bindings for jq, the JSON processor"
|
|
10
|
+
spec.description = "A minimal, security-focused Ruby gem that wraps the jq C library for JSON transformation"
|
|
11
|
+
spec.homepage = "https://github.com/persona-id/jq-ruby"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
18
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
19
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/jq"
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
f.match(%r{^(test|spec|features)/})
|
|
24
|
+
end
|
|
25
|
+
spec.bindir = "exe"
|
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
spec.extensions = ["ext/jq/extconf.rb"]
|
|
29
|
+
|
|
30
|
+
spec.add_dependency "mini_portile2", "~> 2.8"
|
|
31
|
+
|
|
32
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
33
|
+
spec.add_development_dependency "rake-compiler", "~> 1.2"
|
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
35
|
+
end
|
data/lib/jq.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# JQ provides Ruby bindings for the jq JSON processor.
|
|
5
|
+
#
|
|
6
|
+
# This gem wraps the jq C library, allowing you to apply jq filters to JSON
|
|
7
|
+
# strings directly from Ruby. It supports all standard jq operations and
|
|
8
|
+
# provides a clean Ruby API with proper error handling.
|
|
9
|
+
#
|
|
10
|
+
# === Thread Safety
|
|
11
|
+
#
|
|
12
|
+
# This gem requires jq 1.7+ for safe multi-threaded operation. Each method
|
|
13
|
+
# call creates an isolated jq_state, and jq 1.7+ includes critical thread
|
|
14
|
+
# safety fixes (PR #2546). Safe to use from multiple threads in MRI Ruby.
|
|
15
|
+
#
|
|
16
|
+
# === Basic Usage
|
|
17
|
+
#
|
|
18
|
+
# require 'jq'
|
|
19
|
+
#
|
|
20
|
+
# # Simple filtering
|
|
21
|
+
# JQ.filter('{"name":"Alice"}', '.name')
|
|
22
|
+
# # => "\"Alice\""
|
|
23
|
+
#
|
|
24
|
+
# # With options
|
|
25
|
+
# JQ.filter('{"name":"Alice"}', '.name', raw_output: true)
|
|
26
|
+
# # => "Alice"
|
|
27
|
+
#
|
|
28
|
+
# # Multiple outputs
|
|
29
|
+
# JQ.filter('[1,2,3]', '.[]', multiple_outputs: true)
|
|
30
|
+
# # => ["1", "2", "3"]
|
|
31
|
+
#
|
|
32
|
+
# === Error Handling
|
|
33
|
+
#
|
|
34
|
+
# All jq-related errors inherit from JQ::Error:
|
|
35
|
+
#
|
|
36
|
+
# begin
|
|
37
|
+
# JQ.filter('invalid', '.')
|
|
38
|
+
# rescue JQ::ParseError => e
|
|
39
|
+
# puts "Invalid JSON: #{e.message}"
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# @see https://jqlang.github.io/jq/ jq documentation
|
|
43
|
+
#
|
|
44
|
+
module JQ
|
|
45
|
+
##
|
|
46
|
+
# The gem version number
|
|
47
|
+
#
|
|
48
|
+
VERSION = "1.0.0"
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Base exception class for all jq-related errors.
|
|
52
|
+
#
|
|
53
|
+
# All other jq exception classes inherit from this, allowing you to
|
|
54
|
+
# rescue all jq errors with a single rescue clause:
|
|
55
|
+
#
|
|
56
|
+
# begin
|
|
57
|
+
# JQ.filter(json, filter)
|
|
58
|
+
# rescue JQ::Error => e
|
|
59
|
+
# puts "JQ error: #{e.message}"
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
class Error < StandardError; end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Raised when a jq filter expression fails to compile.
|
|
66
|
+
#
|
|
67
|
+
# This typically indicates a syntax error in the filter expression:
|
|
68
|
+
#
|
|
69
|
+
# JQ.filter('{}', '. @@@ .')
|
|
70
|
+
# # raises JQ::CompileError: Syntax error in jq filter
|
|
71
|
+
#
|
|
72
|
+
class CompileError < Error; end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Raised when a jq filter fails during execution.
|
|
76
|
+
#
|
|
77
|
+
# This typically indicates a type mismatch or invalid operation:
|
|
78
|
+
#
|
|
79
|
+
# JQ.filter('42', '.[]')
|
|
80
|
+
# # raises JQ::RuntimeError: Cannot iterate over number
|
|
81
|
+
#
|
|
82
|
+
class RuntimeError < Error; end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Raised when JSON input is invalid or malformed.
|
|
86
|
+
#
|
|
87
|
+
# This indicates the input string is not valid JSON:
|
|
88
|
+
#
|
|
89
|
+
# JQ.filter('not json', '.')
|
|
90
|
+
# # raises JQ::ParseError: Invalid JSON input
|
|
91
|
+
#
|
|
92
|
+
class ParseError < Error; end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
require_relative "jq/jq_ext"
|
|
97
|
+
rescue LoadError => e
|
|
98
|
+
raise LoadError, "Failed to load jq extension. " \
|
|
99
|
+
"Please run 'rake compile' to build the extension. " \
|
|
100
|
+
"Original error: #{e.message}"
|
|
101
|
+
end
|
data/sig/jq.rbs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JQ
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
# Apply a jq filter to JSON input
|
|
7
|
+
#
|
|
8
|
+
# @param json The JSON input as a string
|
|
9
|
+
# @param filter The jq filter expression
|
|
10
|
+
# @param raw_output Return raw strings without JSON encoding (jq -r)
|
|
11
|
+
# @param compact_output Output compact JSON (default: true). Set to false for pretty output
|
|
12
|
+
# @param sort_keys Sort object keys (jq -S)
|
|
13
|
+
# @param multiple_outputs Return array of all results instead of first only
|
|
14
|
+
# @return The filtered result as JSON string, or array of strings if multiple_outputs
|
|
15
|
+
def self.filter: (String json, String filter,
|
|
16
|
+
?raw_output: bool,
|
|
17
|
+
?compact_output: bool,
|
|
18
|
+
?sort_keys: bool,
|
|
19
|
+
?multiple_outputs: false) -> String
|
|
20
|
+
| (String json, String filter,
|
|
21
|
+
?raw_output: bool,
|
|
22
|
+
?compact_output: bool,
|
|
23
|
+
?sort_keys: bool,
|
|
24
|
+
multiple_outputs: true) -> Array[String]
|
|
25
|
+
|
|
26
|
+
# Validate a jq filter expression
|
|
27
|
+
#
|
|
28
|
+
# @param filter The jq filter expression to validate
|
|
29
|
+
# @return true if valid
|
|
30
|
+
# @raise [CompileError] if invalid
|
|
31
|
+
def self.validate_filter!: (String filter) -> true
|
|
32
|
+
|
|
33
|
+
# Base exception class for all jq-related errors
|
|
34
|
+
class Error < StandardError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised when a jq filter fails to compile
|
|
38
|
+
class CompileError < Error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Raised when a jq filter execution fails at runtime
|
|
42
|
+
class RuntimeError < Error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Raised when JSON input is invalid
|
|
46
|
+
class ParseError < Error
|
|
47
|
+
end
|
|
48
|
+
end
|