yeptris 0.1.14.2 → 0.1.15.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3f917a019fa1768553b841e4fcbcc72d736b9e47c81980eb5a2bcd029d05790
4
- data.tar.gz: 56e41fcf4c12527877d50d7178d830341656975ff57c5b6d5292c1ff7c77088a
3
+ metadata.gz: d146f3183f11d9eb980be238bec31b1f7c801c5a72480318a3cbb625d81ca8a1
4
+ data.tar.gz: 69416e379b25969d66e7f331db7a8da17dd43fd1cff315b802df9f7babd8adb3
5
5
  SHA512:
6
- metadata.gz: 6d85cb48948548f7d22ad9f9339a0c7634bfe9d82bd2806c1a3579047bf59de74a64a18204977af12fa98cd8315fa39f0bc11506f571d64b9fd0f2ed701612af
7
- data.tar.gz: e46055db7592ebe92447134c377a0d3bb5e862e1a4b71ccaec918fa1335db1d36c87f76a2870827e35ee9c935fe0c512e61a03696e45112104cfd573c1ec544a
6
+ metadata.gz: 941936e4141c052a1c5faddd116c746333e35e1de32d6ac326dde1db667986a2fbcd658eb3665cecdb66568331f5e66c12ebc7e0443c5e11958129d03f48c8fd
7
+ data.tar.gz: 6d62eeaf6c3c1277ee44d9b54f5a3346d7f12ed4cdaa0665ae5641beff714d8ce3070d56068217a4626d3a4cd01a128c0cea77ce90b99377b736c9807b513833
@@ -26,6 +26,8 @@ typedef struct {
26
26
  int depth;
27
27
  int err;
28
28
  rb_encoding* enc;
29
+ int strict_dup; /* json gem >= 3: duplicate keys raise (issue #37) */
30
+ VALUE dup_key;
29
31
  kcent kc[YEP_KC];
30
32
  } jr;
31
33
 
@@ -175,8 +177,14 @@ static VALUE jr_object_body(jr* j, VALUE h_pre) {
175
177
  j->i++;
176
178
  VALUE val = jr_value(j);
177
179
  if (j->err) goto out;
178
- if (yep_ins_mode == YEP_INS_ASET) {
179
- rb_hash_aset(h_pre == Qundef ? (h_pre = rb_hash_new_capa(8)) : h_pre, key, val);
180
+ if (yep_ins_mode == YEP_INS_ASET || j->strict_dup) {
181
+ VALUE h = h_pre == Qundef ? (h_pre = rb_hash_new_capa(8)) : h_pre;
182
+ if (j->strict_dup && !NIL_P(rb_hash_aref(h, key))) {
183
+ j->err = -3;
184
+ j->dup_key = key;
185
+ goto out;
186
+ }
187
+ rb_hash_aset(h, key, val);
180
188
  } else {
181
189
  if (pn + 2 > pcap) {
182
190
  size_t ncap = pcap * 2;
@@ -312,10 +320,11 @@ double yep_rb_scan_time(const char* p, size_t len, int n) {
312
320
  return (double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_nsec - t0.tv_nsec) / 1e9;
313
321
  }
314
322
 
315
- VALUE yep_rb_parse_json(const char* p, size_t len) {
323
+ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
316
324
  jr j;
317
325
  memset(&j, 0, sizeof(j));
318
326
  j.p = p; j.len = len; j.enc = rb_utf8_encoding();
327
+ j.strict_dup = strict_dup;
319
328
  VALUE already = Qtrue;
320
329
  if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
321
330
  VALUE v = jr_value(&j);
@@ -325,6 +334,10 @@ VALUE yep_rb_parse_json(const char* p, size_t len) {
325
334
  if (already == Qfalse) rb_gc_enable();
326
335
  if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
327
336
  if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
337
+ if (j.err == -3) {
338
+ rb_raise(rb_path2class("Yeptris::ParseError"), "duplicate key \"%s\" in JSON object",
339
+ RSTRING_PTR(j.dup_key));
340
+ }
328
341
  if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
329
342
  return v;
330
343
  }
@@ -229,7 +229,7 @@ static const YeptrisVisitVTable k_vt = {
229
229
  };
230
230
 
231
231
  /* fused JSON→Ruby (json_ruby.c) — no vtable, beats JSON.parse */
232
- VALUE yep_rb_parse_json(const char* p, size_t len);
232
+ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup);
233
233
 
234
234
  static VALUE ctx_result(rb_ctx* c, YeptrisStatus st) {
235
235
  if (st != YEPTRIS_OK || c->failed) {
@@ -244,10 +244,12 @@ static VALUE ctx_result(rb_ctx* c, YeptrisStatus st) {
244
244
  return c->root;
245
245
  }
246
246
 
247
- static VALUE native_load_json(VALUE self, VALUE input) {
247
+ static VALUE native_load_json(int argc, VALUE* argv, VALUE self) {
248
248
  (void)self;
249
+ VALUE input, strict;
250
+ rb_scan_args(argc, argv, "11", &input, &strict);
249
251
  StringValue(input);
250
- return yep_rb_parse_json(RSTRING_PTR(input), (size_t)RSTRING_LEN(input));
252
+ return yep_rb_parse_json(RSTRING_PTR(input), (size_t)RSTRING_LEN(input), RTEST(strict));
251
253
  }
252
254
 
253
255
  static VALUE native_load(VALUE self, VALUE input, VALUE schema) {
@@ -412,7 +414,7 @@ RUBY_FUNC_EXPORTED void Init_native(void) {
412
414
  utf8_enc = rb_utf8_encoding();
413
415
  VALUE mYep = rb_define_module("Yeptris");
414
416
  VALUE mNat = rb_define_module_under(mYep, "Native");
415
- rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
417
+ rb_define_singleton_method(mNat, "load_json", native_load_json, -1);
416
418
  rb_define_singleton_method(mNat, "load", native_load, 2);
417
419
  rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
418
420
  rb_define_singleton_method(mNat, "gc_mode", native_gc_mode, 0);
data/lib/yeptris/json.rb CHANGED
@@ -17,6 +17,13 @@ module Yeptris
17
17
  class Error < ::Yeptris::Error; end
18
18
  class ParseError < Error; end
19
19
 
20
+ # json gem 3.0 made duplicate keys an error by DEFAULT; 2.x is
21
+ # last-wins. The parity target is the RESOLVED json gem's own
22
+ # behavior — strictness follows it (issue #37, found by canon's
23
+ # CI where json 3.0.0 resolved while the dev box had 2.x).
24
+ require "json"
25
+ STRICT_DUPLICATE_KEYS = Gem::Version.new(::JSON::VERSION) >= Gem::Version.new("3")
26
+
20
27
  module_function
21
28
 
22
29
  def load(source)
@@ -24,7 +31,7 @@ module Yeptris
24
31
  source = source.to_s
25
32
  if defined?(::Yeptris::Native)
26
33
  begin
27
- return ::Yeptris::Native.load_json(source)
34
+ return ::Yeptris::Native.load_json(source, STRICT_DUPLICATE_KEYS)
28
35
  rescue ::Yeptris::ParseError => e
29
36
  raise ParseError, e.message
30
37
  end
@@ -52,7 +59,7 @@ module Yeptris
52
59
  raise ParseError, ::Yeptris::FFI.last_error_message if st != ::Yeptris::FFI::OK
53
60
 
54
61
  begin
55
- walk_strict(cols)
62
+ walk_strict(cols, STRICT_DUPLICATE_KEYS)
56
63
  ensure
57
64
  ::Yeptris::FFI.yeptris_value_free_columns(cols)
58
65
  end
@@ -65,7 +72,7 @@ module Yeptris
65
72
  # is the strict-JSON one (no ':sym' scan, no y/n quirk, no
66
73
  # dot-required floats). Anchors/aliases/timestamps cannot occur
67
74
  # in strict JSON — reaching them is an internal error.
68
- def walk_strict(cols)
75
+ def walk_strict(cols, strict_dup = STRICT_DUPLICATE_KEYS)
69
76
  n = cols[:count]
70
77
  kinds = cols[:kinds].read_bytes(n).unpack("C*")
71
78
  ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
@@ -78,6 +85,7 @@ module Yeptris
78
85
 
79
86
  docs = []
80
87
  stack = []
88
+ key_sets = strict_dup ? [{}] : nil
81
89
  pending_key = nil
82
90
  i = 0
83
91
  while i < n
@@ -88,13 +96,19 @@ module Yeptris
88
96
  place(docs, stack, pending_key) { [] }
89
97
  pending_key = nil
90
98
  when ValueML::MAP_OPEN
99
+ key_sets&.push({})
91
100
  place(docs, stack, pending_key) { {} }
92
101
  pending_key = nil
93
102
  when ValueML::CLOSE
94
103
  stack.pop
104
+ key_sets&.pop
95
105
  when ValueML::V_STR
96
106
  text = arena.byteslice(offs[i], lens[i])
97
107
  if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
108
+ if strict_dup && key_sets.last.key?(text)
109
+ raise ParseError, %(duplicate key "#{text}" in JSON object)
110
+ end
111
+ key_sets&.last&.store(text, true)
98
112
  pending_key = text
99
113
  else
100
114
  # Records carry int64 payloads: an integer-beyond-int64
data/lib/yeptris/node.rb CHANGED
@@ -340,7 +340,16 @@ class Yeptris::Node
340
340
  to_f
341
341
  end
342
342
  when :timestamp then ::Yeptris::Materializer.parse_timestamp(value)
343
- else value
343
+ else
344
+ # beyond int64 the C resolver leaves plain scalars :str; Psych
345
+ # materializes Integer (issue #31) — rebuild from the text
346
+ text = value.to_s
347
+ if style == :plain && text.length > 18 &&
348
+ ::Yeptris::ValueML::PSYCH_INT_SHAPE.match?(text)
349
+ text.delete(",_").to_i
350
+ else
351
+ value
352
+ end
344
353
  end
345
354
  end
346
355
 
@@ -28,7 +28,7 @@ module Yeptris
28
28
  dispatch(flat, arena)
29
29
  self
30
30
  rescue ParseError => e
31
- raise SyntaxError, e.message
31
+ raise SyntaxError.from_parse_error(e)
32
32
  end
33
33
 
34
34
  private
data/lib/yeptris/psych.rb CHANGED
@@ -30,13 +30,30 @@ module Yeptris
30
30
  # object instance exists.
31
31
  autoload :Encodable, "yeptris/psych/encodable"
32
32
  class Error < StandardError; end
33
+ # Psych's exact interface (issue #32): same constructor arity,
34
+ # same reader set (file/line/column/offset/problem/context), same
35
+ # message shape — drop-in consumers' rescues and constructors
36
+ # keep working after the rebind.
33
37
  class SyntaxError < Error
34
- attr_reader :line, :column
38
+ attr_reader :file, :line, :column, :offset, :problem, :context
35
39
 
36
- def initialize(message, line = 0, column = 0)
37
- super(message)
40
+ def initialize(file = nil, line = 0, column = 0, offset = 0, problem = nil, context = nil)
41
+ @file = file
38
42
  @line = line
39
43
  @column = column
44
+ @offset = offset
45
+ @problem = problem
46
+ @context = context
47
+ where = file ? "(#{file})" : "(<unknown>)"
48
+ detail = context ? "#{problem} #{context}" : problem.to_s
49
+ super("#{where}: #{detail} at line #{line} column #{column}")
50
+ end
51
+
52
+ # Structured lift from the C parser's message (carries
53
+ # "line L, column C" detail).
54
+ def self.from_parse_error(error)
55
+ md = /\bline (\d+),? column (\d+)/.match(error.message)
56
+ new(nil, md ? md[1].to_i : 0, md ? md[2].to_i : 0, 0, error.message)
40
57
  end
41
58
  end
42
59
  class BadAlias < Error; end
@@ -84,7 +101,7 @@ module Yeptris
84
101
 
85
102
  Nodes::Builder.document(doc)
86
103
  rescue Yeptris::ParseError => e
87
- raise SyntaxError, e.message
104
+ raise SyntaxError.from_parse_error(e)
88
105
  end
89
106
 
90
107
  def parse_stream(yaml)
@@ -101,7 +118,7 @@ module Yeptris
101
118
  )
102
119
  stream
103
120
  rescue Yeptris::ParseError => e
104
- raise SyntaxError, e.message
121
+ raise SyntaxError.from_parse_error(e)
105
122
  ensure
106
123
  doc&.free if doc && !stream
107
124
  end
@@ -14,6 +14,12 @@ module Yeptris
14
14
  V_INT = 3
15
15
  V_FLOAT = 4
16
16
  V_STR = 5
17
+
18
+ # Psych's integer shape (scalar_scanner): sign optional, no
19
+ # leading zero, '_' / ',' separators only between digits. Beyond
20
+ # int64 the C resolver leaves the scalar a string; Psych
21
+ # materializes Integer (issue #31) — every walk rebuilds it.
22
+ PSYCH_INT_SHAPE = /\A[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*)\z/.freeze
17
23
  V_TS = 6
18
24
  SEQ_OPEN = 7
19
25
  MAP_OPEN = 8
@@ -149,7 +155,10 @@ module Yeptris
149
155
  text = arena.byteslice(flat[i + OFF], flat[i + LEN])
150
156
  # implicit-plain ':name' scans to a Symbol (Psych's
151
157
  # ScalarScanner); quoted ':x' stays a String
152
- v = if flat[i + B] == 1 && text.length > 1 &&
158
+ v = if flat[i + B] == 1 && text.length > 18 &&
159
+ PSYCH_INT_SHAPE.match?(text)
160
+ text.delete(",_").to_i
161
+ elsif flat[i + B] == 1 && text.length > 1 &&
153
162
  text.start_with?(":") && !text.start_with?("::")
154
163
  text[1..].to_sym
155
164
  else
@@ -290,7 +299,10 @@ module Yeptris
290
299
  case kinds[i]
291
300
  when V_STR
292
301
  text = arena.byteslice(offs[i], lens[i])
293
- v = if bools[i] == 1 && text.length > 1 &&
302
+ v = if bools[i] == 1 && text.length > 18 &&
303
+ PSYCH_INT_SHAPE.match?(text)
304
+ text.delete(",_").to_i
305
+ elsif bools[i] == 1 && text.length > 1 &&
294
306
  text.start_with?(":") && !text.start_with?("::")
295
307
  text[1..].to_sym
296
308
  else
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.1.14.2".freeze
6
+ VERSION = "0.1.15.1".freeze
7
7
  # The error hierarchy lives in THIS file (the parent namespace's
8
8
  # own file): nested constants do not trigger a parent-constant
9
9
  # autoload, and the law forbids internal requires — defining the
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14.2
4
+ version: 0.1.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: ffi
@@ -57,6 +58,7 @@ metadata:
57
58
  homepage_uri: https://github.com/leptris/yeptris
58
59
  source_code_uri: https://github.com/leptris/yeptris
59
60
  changelog_uri: https://github.com/leptris/yeptris/releases
61
+ post_install_message:
60
62
  rdoc_options: []
61
63
  require_paths:
62
64
  - lib
@@ -71,7 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
73
  - !ruby/object:Gem::Version
72
74
  version: '0'
73
75
  requirements: []
74
- rubygems_version: 4.0.16
76
+ rubygems_version: 3.5.22
77
+ signing_key:
75
78
  specification_version: 4
76
79
  summary: 'The YAML counterpart of libleptris: ultra-performance YAML 1.2 for Ruby'
77
80
  test_files: []