yeptris 0.6.15.2 → 0.6.16.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: b1326388a6f59887d9b8f541ec6f4c835071ca05f4821d429a82b8acc1a990e2
4
- data.tar.gz: 521f5abbcebb9611a0c67699b1952e72d3790d228fe606148f095d622f5b4387
3
+ metadata.gz: 36b4d1444bde48fba47b0fd89851f5ebe8cd7a666c7e3faa8652caf4bb37a60f
4
+ data.tar.gz: 7f113394e8f82d502425610637641716af695a835ba01f6ce5e611c5073c6d55
5
5
  SHA512:
6
- metadata.gz: 7b6e36f35a32d480af8635023bbaf173194e8295e34851ae53ec8121cdf872e9d71c4877f70cc94d1ee62d0d22e4f74e868edc7746b2fbbd9d66af47f31daad0
7
- data.tar.gz: 21a442ef4c99c57f46b837af822db3d98434c476b5a0b7c56d52f03bbfac0c8849be28b2a7fd2109a95b15a80ea6eda9116c3631ff5c6faff2d134e5b2a861fc
6
+ metadata.gz: cec260d2c0b2c10d152336d0809eca8d9d15c4fc8e52ddac94fa01b8850c3f59cebb74f0ddd78b48f751ce9fb6a1f74e6f9646d81a237144645810949c93b7a0
7
+ data.tar.gz: efa7dbc2609e25b0d25bb0d93c7d877223808d88343299e7dfd60ed9442eb31806347a4aa1f255452bfc219af2a5d4c34b66f5d50346d839de59ee256dafe0da
@@ -17,6 +17,14 @@
17
17
  #include <stdlib.h>
18
18
  #include <string.h>
19
19
 
20
+ static rb_encoding* cr_utf8_enc;
21
+
22
+ /* Map keys repeat across records in CBOR corpora; interning shares one
23
+ * VALUE per distinct key (the json_ruby.c on_key pattern, #157). */
24
+ static VALUE cr_key_str(const char* p, size_t len) {
25
+ return rb_enc_interned_str(p, (long)len, cr_utf8_enc);
26
+ }
27
+
20
28
  #include "dom/dom.h"
21
29
  #include "doc.h" /* the public YeptrisDocument wrapper: ->dom */
22
30
  #include <yeptris/cbor.h>
@@ -91,8 +99,17 @@ static VALUE cr_walk(const yep_dom* d, uint32_t id) {
91
99
  VALUE h = HASH_NEW_CAPA(pairs);
92
100
  uint32_t c = n->first_child;
93
101
  for (long i = 0; i < pairs && c != UINT32_MAX; i++) {
94
- VALUE k = cr_walk(d, c);
95
- c = d->nodes[c].next_sibling;
102
+ VALUE k;
103
+ const yep_dnode* kn = &d->nodes[c];
104
+ if (kn->kind == YEP_DOM_SCALAR && kn->tag_id == YEPTRIS_TAG_STR) {
105
+ uint32_t klen = 0;
106
+ const char* kp = cr_view(d, kn->value, &klen);
107
+ k = cr_key_str(kp, klen);
108
+ c = kn->next_sibling;
109
+ } else {
110
+ k = cr_walk(d, c);
111
+ c = d->nodes[c].next_sibling;
112
+ }
96
113
  VALUE v = (c != UINT32_MAX) ? cr_walk(d, c) : Qnil;
97
114
  if (c != UINT32_MAX) {
98
115
  c = d->nodes[c].next_sibling;
@@ -107,6 +124,9 @@ static VALUE cr_walk(const yep_dom* d, uint32_t id) {
107
124
  }
108
125
 
109
126
  VALUE yep_rb_cbor_load(const char* p, size_t len, int strict) {
127
+ if (cr_utf8_enc == NULL) {
128
+ cr_utf8_enc = rb_utf8_encoding();
129
+ }
110
130
  YeptrisStatus st = YEPTRIS_OK;
111
131
  /* the DOM borrows the input buffer zero-copy; the walk ALLOCATES,
112
132
  * and a GC compaction mid-walk moves the caller's String — the
@@ -8,6 +8,11 @@ class Yeptris::Document
8
8
  # explicit free path and the finalizer both flip the same flag.
9
9
  Freed = Struct.new(:state) # :alive | :freed
10
10
 
11
+ # The raw C handle — the Yeptris::Node wrappers cache by its
12
+ # address; stream-children wrappers (Document.without_finalizer)
13
+ # carry it without registering a finalizer (#182).
14
+ attr_reader :c_ptr
15
+
11
16
  # The schema this document was parsed with (a parse property).
12
17
  attr_reader :parse_schema
13
18
 
@@ -80,6 +85,18 @@ class Yeptris::Document
80
85
  new(c_ptr, Freed.new(:alive), schema)
81
86
  end
82
87
 
88
+ # #182: the stream-children wrapper owns NOTHING (the parent
89
+ # Document wrapper is the sole freer of the C memory). A plain
90
+ # Document.new would register a finalizer on a c_ptr we don't own;
91
+ # a parse_stream on a multi-doc stream would free the same pointer
92
+ # three times at GC → SIGABRT. This factory builds the wrapper
93
+ # WITHOUT a finalizer; the wrapper's lifetime follows the owner.
94
+ def self.without_finalizer(c_ptr)
95
+ doc = new(c_ptr)
96
+ ObjectSpace.undefine_finalizer(doc)
97
+ doc
98
+ end
99
+
83
100
  def ensure_alive!
84
101
  raise Yeptris::FreedError, "document is freed" if @freed.state == :freed
85
102
  end
data/lib/yeptris/psych.rb CHANGED
@@ -291,7 +291,15 @@ module Yeptris
291
291
  raise SyntaxError.from_parse_error(e)
292
292
  end
293
293
 
294
- def parse_stream(yaml)
294
+ def parse_stream(yaml, &block)
295
+ # stdlib yields each document to a block if one was given (and
296
+ # the stream it returns holds the same children either way).
297
+ # #182: the wrapper is the sole owner of the C memory — the
298
+ # stream merely REFERENCES it; the wrapper's own finalizer
299
+ # handles GC-free. (The block-yielding form is tracked under
300
+ # #179: the current implementation materializes eagerly; the
301
+ # block is only honored as a no-op convenience.)
302
+ _ = block # reserved for the future yielding form (#179)
295
303
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
296
304
  return nil if doc.nil? || doc.document_count.zero?
297
305
 
@@ -308,6 +316,8 @@ module Yeptris
308
316
  rescue Yeptris::ParseError => e
309
317
  raise SyntaxError.from_parse_error(e)
310
318
  ensure
319
+ # the returned stream OWNS the doc's lifetime (its free delegates
320
+ # to the owner); only free here when the stream wasn't built
311
321
  doc&.free if doc && !stream
312
322
  end
313
323
 
@@ -522,8 +532,12 @@ module Yeptris
522
532
  # stream owns the yeptris document)
523
533
  def document_stream_child(doc, index)
524
534
  root = doc.root(index)
525
- d = Document.new
526
- d.handle = root&.document
535
+ # #182's root cause: three per-stream Document wrappers all
536
+ # capture the same c_ptr in their finalizers → three frees.
537
+ # The stream-children WRAPPER owns nothing (the owner wrapper
538
+ # is the real Document; this one just references it). Build
539
+ # a non-owning wrapper with no finalizer.
540
+ d = Document.send(:new, root&.document, false)
527
541
  d.children << node(root) if root
528
542
  d
529
543
  end
@@ -66,6 +66,92 @@ module Yeptris
66
66
  end
67
67
  end
68
68
 
69
+ # #184 (lutaml-model KV path): zip Schema columns into record
70
+ # hashes ready for Serializable.instantiate. Mapping root → one
71
+ # hash; sequence-of-mappings → one hash per element; nested
72
+ # mappings → nested hashes. Returns Array<Hash> always. when_attribute
73
+ # / polymorphic stay out of scope (interpretive fallback).
74
+ def load_records(source, schema: :core_12, desc:, capacity: 64)
75
+ cols = load(source, schema: schema, desc: desc, capacity: capacity)
76
+ zip_records(desc, cols)
77
+ end
78
+
79
+ def zip_records(desc, cols)
80
+ root = desc[0] || {}
81
+ case root[:kind]
82
+ when :sequence
83
+ child_start = root[:child_index] || 1
84
+ child_count = root[:child_count] || 0
85
+ return [] if child_count.zero?
86
+
87
+ child = desc[child_start]
88
+ if child && child[:kind] == :mapping
89
+ field_start = child[:child_index] || (child_start + 1)
90
+ field_count = child[:child_count] || 0
91
+ fields = desc[field_start, field_count] || []
92
+ field_cols = cols[field_start, field_count] || []
93
+ nrows = field_cols.map(&:length).max || 0
94
+ Array.new(nrows) do |r|
95
+ h = {}
96
+ fields.each_with_index do |f, i|
97
+ next unless f[:wire_name]
98
+ col = field_cols[i] || []
99
+ # ABSENT keys are OMITTED, not nil-filled (lutaml-model's
100
+ # load-bearing edge: absent seeds defaults +
101
+ # using_default?; explicit nil is a present value with
102
+ # different render semantics). The C columns fill in
103
+ # document order — col.length > r means present.
104
+ next if col.length <= r
105
+
106
+ h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[r])
107
+ end
108
+ h
109
+ end
110
+ else
111
+ (cols[child_start] || []).map { |v| { value: v } }
112
+ end
113
+ when :mapping
114
+ field_start = root[:child_index] || 1
115
+ field_count = root[:child_count] || 0
116
+ fields = desc[field_start, field_count] || []
117
+ field_cols = cols[field_start, field_count] || []
118
+ h = {}
119
+ fields.each_with_index do |f, i|
120
+ next unless f[:wire_name]
121
+ col = field_cols[i] || []
122
+ next if col.empty? # absent: omitted, not nil-filled (#184)
123
+
124
+ h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[0])
125
+ end
126
+ [h]
127
+ else
128
+ [{ value: cols[0]&.first }]
129
+ end
130
+ end
131
+ private_class_method :zip_records
132
+
133
+ def materialize_field(field, desc, cols, cell)
134
+ case field[:kind]
135
+ when :mapping
136
+ start = field[:child_index] || 0
137
+ count = field[:child_count] || 0
138
+ return nil if count.zero? || cell.nil?
139
+
140
+ nested = {}
141
+ desc[start, count]&.each_with_index do |nf, i|
142
+ next unless nf[:wire_name]
143
+ ncol = cols[start + i] || []
144
+ nested[nf[:wire_name].to_sym] = ncol.is_a?(Array) ? (ncol[0] rescue cell) : cell
145
+ end
146
+ nested
147
+ when :sequence
148
+ cell.is_a?(Array) ? cell : Array(cell)
149
+ else
150
+ cell
151
+ end
152
+ end
153
+ private_class_method :materialize_field
154
+
69
155
  def compile(desc)
70
156
  desc.map do |n|
71
157
  { kind: KIND.fetch(n.fetch(:kind)),
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.6.15.2".freeze
6
+ VERSION = "0.6.16.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
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.15
6
+ VERSION 0.6.16
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -134,8 +134,17 @@ static void wr_indent(yep_writer* w, int depth) {
134
134
 
135
135
  static void emit_dq(yep_writer* w, const char* p, uint32_t n) {
136
136
  wr_byte(w, '"');
137
+ uint32_t run = 0;
137
138
  for (uint32_t i = 0; i < n; i++) {
138
139
  unsigned char c = (unsigned char)p[i];
140
+ if (c >= 0x20 && c != '"' && c != '\\' && c != 0x7f) {
141
+ run++;
142
+ continue;
143
+ }
144
+ if (run) {
145
+ wr_put(w, p + i - run, run);
146
+ run = 0;
147
+ }
139
148
  if (c == '"' || c == '\\') {
140
149
  wr_byte(w, '\\');
141
150
  wr_byte(w, (char)c);
@@ -161,10 +170,11 @@ static void emit_dq(yep_writer* w, const char* p, uint32_t n) {
161
170
  char hex[4] = {'\\', 'x', hd[(c >> 4) & 0xf], hd[c & 0xf]};
162
171
  wr_put(w, hex, 4);
163
172
  }
164
- } else {
165
- wr_byte(w, (char)c);
166
173
  }
167
174
  }
175
+ if (run) {
176
+ wr_put(w, p + n - run, run);
177
+ }
168
178
  wr_byte(w, '"');
169
179
  }
170
180
 
@@ -315,6 +325,10 @@ enum { YEP_SC_NOTHING = 0, YEP_SC_PLAIN, YEP_SC_SQ, YEP_SC_DQ, YEP_SC_LITERAL };
315
325
 
316
326
  static uint8_t sc_route(yep_writer* w, const char* p, uint32_t len, int as_key, uint8_t sty_in,
317
327
  int has_tag) {
328
+ if ((sty_in == 1) && len > 0 &&
329
+ (as_key ? yep_style_plain_key_safe(p, len) : yep_style_plain_safe(p, len))) {
330
+ return YEP_SC_PLAIN; /* plain-safe implies no breaks: memchr skipped */
331
+ }
318
332
  int multiline = (len > 0 && memchr(p, '\n', len) != NULL);
319
333
  int blockable = 0;
320
334
  if (multiline && !as_key) {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.15.2
4
+ version: 0.6.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.