yeptris 0.1.11.0 → 0.1.12.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/README.adoc +31 -2
- data/ext/yeptris_native/extconf.rb +39 -0
- data/ext/yeptris_native/json_ruby.c +203 -0
- data/ext/yeptris_native/yeptris_native.c +333 -0
- data/lib/yeptris/psych/encodable.rb +14 -0
- data/lib/yeptris/psych/visitors.rb +83 -64
- data/lib/yeptris/psych.rb +10 -1
- data/lib/yeptris/yaml.rb +27 -0
- data/lib/yeptris.rb +10 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fa74d905a2d19dbeaf70381760c3a629738808e507d0ef9f33c9e266b756854
|
|
4
|
+
data.tar.gz: 2d3fb1c11b0d2dbb88c5bb75e837f41abe971992c1940a8c5be191b0168411b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e621ced9cf2707ada102a8633f7f75f597f63488f2a66da4efdb0dd3e292ef0719bbba509db321ca9e5cfd38f455a732a90cb3d1709b79f4ae7cab5ef0dd48da
|
|
7
|
+
data.tar.gz: 9d5efd07ce2e2f498320133776e3b0ed08e1fec4977a7e5cd1b607616630377bc29f7d5708e00c058c2f7d97022a6b668d6e3865c33da17a7d9c8ba71d445f2e
|
data/README.adoc
CHANGED
|
@@ -52,6 +52,33 @@ Handles are document-scoped: `Document#free` releases everything
|
|
|
52
52
|
(one C call), a GC finalizer backs it up, and any use after free
|
|
53
53
|
raises `Yeptris::FreedError` — never a segfault.
|
|
54
54
|
|
|
55
|
+
== Native materializer (opt-in): faster than JSON.parse
|
|
56
|
+
|
|
57
|
+
For JSON-shaped input, the binding can beat Ruby's own `JSON.parse`:
|
|
58
|
+
|
|
59
|
+
....
|
|
60
|
+
cd ext/yeptris_native
|
|
61
|
+
YEPTRIS_LIB_PATH=/path/to/libyeptris.dylib ruby extconf.rb && make
|
|
62
|
+
cp native.bundle ../../lib/yeptris/ # or .so on Linux
|
|
63
|
+
....
|
|
64
|
+
|
|
65
|
+
`require "yeptris"` picks it up automatically (a missing build falls
|
|
66
|
+
back to the FFI Marshal ladder silently). Measured on the 152 KB /
|
|
67
|
+
29.4k-value JSON corpus (mean of 300, Ruby 3.4):
|
|
68
|
+
|
|
69
|
+
....
|
|
70
|
+
JSON.parse mean 1.597 ms
|
|
71
|
+
Yeptris::YAML.load mean 1.145 ms (0.72x — faster than JSON.parse)
|
|
72
|
+
Psych.load mean 40.60 ms (35x slower than yeptris)
|
|
73
|
+
....
|
|
74
|
+
|
|
75
|
+
The extension is a fused RFC 8259 → `VALUE` parser (one pass, no
|
|
76
|
+
intermediate records): libyeptris scan kernels tokenize, the Ruby C
|
|
77
|
+
API allocates, repeated keys/tokens share one frozen `String`, and GC
|
|
78
|
+
is paused for the duration. YAML inputs keep the FFI ladder so
|
|
79
|
+
timestamps, aliases, and Psych's scalar quirks resolve through the
|
|
80
|
+
same path `Psych.load` uses.
|
|
81
|
+
|
|
55
82
|
== Shipped beyond the original plan
|
|
56
83
|
|
|
57
84
|
* The columnar value drain (bulk, pre-converted typed columns — the
|
|
@@ -65,8 +92,10 @@ raises `Yeptris::FreedError` — never a segfault.
|
|
|
65
92
|
object links; merge keys and timestamps return UNSUPPORTED for the
|
|
66
93
|
record-walk fallback.
|
|
67
94
|
* `Yeptris::Psych` — the drop-in namespace with the ported Psych
|
|
68
|
-
suite (143 specs), `.tml` corpora conformance, and the
|
|
69
|
-
|
|
95
|
+
suite (143 specs), `.tml` corpora conformance, and the Encodable
|
|
96
|
+
object protocol (`include Yeptris::Psych::Encodable` +
|
|
97
|
+
`encode_with`/`init_with` — no `respond_to?`, no ivar reflection;
|
|
98
|
+
the library never reaches into an object's internals).
|
|
70
99
|
* Lockstep versioning with the C library
|
|
71
100
|
(`{c-semver}.{gem-patch}`); releases published from the C repo's
|
|
72
101
|
workflow through the RubyGems trusted publisher.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
|
|
5
|
+
lib_path = ENV["YEPTRIS_LIB_PATH"]
|
|
6
|
+
candidates = []
|
|
7
|
+
if lib_path
|
|
8
|
+
candidates << File.dirname(lib_path)
|
|
9
|
+
candidates << lib_path if File.directory?(lib_path)
|
|
10
|
+
end
|
|
11
|
+
candidates << File.expand_path("../../../../yeptris/build-validate/src", __dir__)
|
|
12
|
+
candidates << File.expand_path("../../../../yeptris/build/src", __dir__)
|
|
13
|
+
candidates << File.expand_path("../../../yeptris/build-validate/src", __dir__)
|
|
14
|
+
candidates << File.expand_path("../../../yeptris/build/src", __dir__)
|
|
15
|
+
|
|
16
|
+
src_root = [
|
|
17
|
+
File.expand_path("../../../../yeptris/src", __dir__),
|
|
18
|
+
File.expand_path("../../../yeptris/src", __dir__),
|
|
19
|
+
].find { |d| d && File.directory?(File.join(d, "include")) }
|
|
20
|
+
abort "yeptris sources not found" unless src_root
|
|
21
|
+
|
|
22
|
+
$INCFLAGS << " -I#{src_root}/include -I#{src_root}/yeptris"
|
|
23
|
+
%w[build-validate/generated build/generated].each do |g|
|
|
24
|
+
d = File.join(File.dirname(src_root), g)
|
|
25
|
+
$INCFLAGS << " -I#{d}" if File.directory?(d)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
libdir = candidates.find do |d|
|
|
29
|
+
d && File.directory?(d) && Dir[File.join(d, "libyeptris*.{dylib,so,dll}")].any?
|
|
30
|
+
end
|
|
31
|
+
abort "libyeptris not found (set YEPTRIS_LIB_PATH)" unless libdir
|
|
32
|
+
|
|
33
|
+
$LIBPATH << libdir
|
|
34
|
+
$LDFLAGS << " -Wl,-rpath,#{libdir}" if RUBY_PLATFORM =~ /linux|darwin/
|
|
35
|
+
have_library("yeptris", "yep_json_string") or abort "yep_json_string not exported — rebuild libyeptris"
|
|
36
|
+
have_library("yeptris", "yeptris_visit_json") or abort "yeptris_visit_json missing"
|
|
37
|
+
|
|
38
|
+
$CFLAGS << " -O3 -fvisibility=hidden" unless RUBY_PLATFORM =~ /mswin|mingw/
|
|
39
|
+
create_makefile("yeptris/native")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/* json_ruby.c — fused RFC 8259 → Ruby VALUE (TODO.restructure/22). */
|
|
2
|
+
#include <ruby.h>
|
|
3
|
+
#include <ruby/encoding.h>
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
#include <string.h>
|
|
6
|
+
#include "parse/numbers.h"
|
|
7
|
+
#include "parse/scalars.h"
|
|
8
|
+
#include "scan/json.h"
|
|
9
|
+
|
|
10
|
+
#define YEP_JR_MAX 1000
|
|
11
|
+
#define YEP_KC 256
|
|
12
|
+
|
|
13
|
+
typedef struct {
|
|
14
|
+
uint64_t h;
|
|
15
|
+
uint32_t len;
|
|
16
|
+
VALUE v;
|
|
17
|
+
} kcent;
|
|
18
|
+
|
|
19
|
+
typedef struct {
|
|
20
|
+
const char* p;
|
|
21
|
+
size_t len;
|
|
22
|
+
size_t i;
|
|
23
|
+
char* scratch;
|
|
24
|
+
size_t scratch_cap;
|
|
25
|
+
int depth;
|
|
26
|
+
int err;
|
|
27
|
+
rb_encoding* enc;
|
|
28
|
+
kcent kc[YEP_KC];
|
|
29
|
+
} jr;
|
|
30
|
+
|
|
31
|
+
static VALUE jr_value(jr* j);
|
|
32
|
+
|
|
33
|
+
static void jr_ws(jr* j) {
|
|
34
|
+
while (j->i < j->len) {
|
|
35
|
+
unsigned char c = (unsigned char)j->p[j->i];
|
|
36
|
+
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') j->i++;
|
|
37
|
+
else break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static uint64_t jr_hash(const char* sp, long sl) {
|
|
42
|
+
/* FNV-1a 64 — cheap, good enough for short keys */
|
|
43
|
+
uint64_t h = 14695981039346656037ull;
|
|
44
|
+
for (long i = 0; i < sl; i++) {
|
|
45
|
+
h ^= (unsigned char)sp[i];
|
|
46
|
+
h *= 1099511628211ull;
|
|
47
|
+
}
|
|
48
|
+
h ^= (uint64_t)sl;
|
|
49
|
+
return h;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static VALUE jr_cached(jr* j, const char* sp, long sl) {
|
|
53
|
+
uint64_t h = jr_hash(sp, sl);
|
|
54
|
+
uint32_t slot = (uint32_t)(h & (YEP_KC - 1));
|
|
55
|
+
kcent* e = &j->kc[slot];
|
|
56
|
+
if (e->v != 0 && e->h == h && e->len == (uint32_t)sl &&
|
|
57
|
+
(long)RSTRING_LEN(e->v) == sl &&
|
|
58
|
+
memcmp(RSTRING_PTR(e->v), sp, (size_t)sl) == 0) {
|
|
59
|
+
return e->v;
|
|
60
|
+
}
|
|
61
|
+
VALUE s = rb_enc_str_new(sp, sl, j->enc);
|
|
62
|
+
rb_str_freeze(s);
|
|
63
|
+
e->h = h;
|
|
64
|
+
e->len = (uint32_t)sl;
|
|
65
|
+
e->v = s;
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static VALUE jr_str(jr* j, int as_key) {
|
|
70
|
+
size_t start = j->i, close = 0;
|
|
71
|
+
int has_esc = 0;
|
|
72
|
+
if (!yep_json_string(j->p, j->len, &j->i, &close, &has_esc)) { j->err = -2; return Qnil; }
|
|
73
|
+
const char* sp; long sl;
|
|
74
|
+
if (has_esc) {
|
|
75
|
+
uint32_t span = (uint32_t)(close - start - 1);
|
|
76
|
+
if (span + 1 > j->scratch_cap) {
|
|
77
|
+
size_t cap = j->scratch_cap ? j->scratch_cap : 64;
|
|
78
|
+
while (cap < span + 1) cap *= 2;
|
|
79
|
+
char* ns = realloc(j->scratch, cap);
|
|
80
|
+
if (!ns) { j->err = -1; return Qnil; }
|
|
81
|
+
j->scratch = ns; j->scratch_cap = cap;
|
|
82
|
+
}
|
|
83
|
+
sl = (long)yep_finish_double_into(j->p, (uint32_t)(start + 1), (uint32_t)close, j->scratch, span);
|
|
84
|
+
sp = j->scratch;
|
|
85
|
+
} else {
|
|
86
|
+
sp = j->p + start + 1;
|
|
87
|
+
sl = (long)(close - start - 1);
|
|
88
|
+
}
|
|
89
|
+
if (as_key || sl <= 2) return jr_cached(j, sp, sl);
|
|
90
|
+
return rb_enc_str_new(sp, sl, j->enc);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static VALUE jr_num(jr* j) {
|
|
94
|
+
size_t start = j->i;
|
|
95
|
+
if (!yep_json_number(j->p, j->len, &j->i)) { j->err = -2; return Qnil; }
|
|
96
|
+
const char* s = j->p + start;
|
|
97
|
+
uint32_t n = (uint32_t)(j->i - start);
|
|
98
|
+
int is_float = 0, neg = 0;
|
|
99
|
+
uint32_t k = 0;
|
|
100
|
+
if (s[0] == '-') { neg = 1; k = 1; }
|
|
101
|
+
for (; k < n; k++) {
|
|
102
|
+
char c = s[k];
|
|
103
|
+
if (c == '.' || c == 'e' || c == 'E') { is_float = 1; break; }
|
|
104
|
+
}
|
|
105
|
+
if (!is_float && n - (uint32_t)neg <= 18) {
|
|
106
|
+
int64_t v = 0;
|
|
107
|
+
for (k = (uint32_t)neg; k < n; k++) v = v * 10 + (s[k] - '0');
|
|
108
|
+
if (neg) v = -v;
|
|
109
|
+
return LL2NUM(v);
|
|
110
|
+
}
|
|
111
|
+
if (is_float) {
|
|
112
|
+
double d = 0.0;
|
|
113
|
+
if (yep_num_f64(s, n, &d) != 0) { j->err = -2; return Qnil; }
|
|
114
|
+
return DBL2NUM(d);
|
|
115
|
+
}
|
|
116
|
+
int64_t v = 0;
|
|
117
|
+
if (yep_num_i64(s, n, &v) != 0) {
|
|
118
|
+
double d = 0.0;
|
|
119
|
+
if (yep_num_f64(s, n, &d) != 0) { j->err = -2; return Qnil; }
|
|
120
|
+
return DBL2NUM(d);
|
|
121
|
+
}
|
|
122
|
+
return LL2NUM(v);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static VALUE jr_object(jr* j) {
|
|
126
|
+
j->i++; j->depth++;
|
|
127
|
+
VALUE h = rb_hash_new_capa(8);
|
|
128
|
+
jr_ws(j);
|
|
129
|
+
if (j->i < j->len && j->p[j->i] == '}') { j->i++; j->depth--; return h; }
|
|
130
|
+
for (;;) {
|
|
131
|
+
jr_ws(j);
|
|
132
|
+
if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; return Qnil; }
|
|
133
|
+
VALUE key = jr_str(j, 1);
|
|
134
|
+
if (j->err) return Qnil;
|
|
135
|
+
jr_ws(j);
|
|
136
|
+
if (j->i >= j->len || j->p[j->i] != ':') { j->err = -2; return Qnil; }
|
|
137
|
+
j->i++;
|
|
138
|
+
VALUE val = jr_value(j);
|
|
139
|
+
if (j->err) return Qnil;
|
|
140
|
+
rb_hash_aset(h, key, val);
|
|
141
|
+
jr_ws(j);
|
|
142
|
+
if (j->i >= j->len) { j->err = -2; return Qnil; }
|
|
143
|
+
if (j->p[j->i] == ',') { j->i++; continue; }
|
|
144
|
+
if (j->p[j->i] == '}') { j->i++; j->depth--; return h; }
|
|
145
|
+
j->err = -2; return Qnil;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static VALUE jr_array(jr* j) {
|
|
150
|
+
j->i++; j->depth++;
|
|
151
|
+
VALUE a = rb_ary_new_capa(8);
|
|
152
|
+
jr_ws(j);
|
|
153
|
+
if (j->i < j->len && j->p[j->i] == ']') { j->i++; j->depth--; return a; }
|
|
154
|
+
for (;;) {
|
|
155
|
+
VALUE v = jr_value(j);
|
|
156
|
+
if (j->err) return Qnil;
|
|
157
|
+
rb_ary_push(a, v);
|
|
158
|
+
jr_ws(j);
|
|
159
|
+
if (j->i >= j->len) { j->err = -2; return Qnil; }
|
|
160
|
+
if (j->p[j->i] == ',') { j->i++; continue; }
|
|
161
|
+
if (j->p[j->i] == ']') { j->i++; j->depth--; return a; }
|
|
162
|
+
j->err = -2; return Qnil;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static VALUE jr_value(jr* j) {
|
|
167
|
+
if (j->depth >= YEP_JR_MAX) { j->err = -2; return Qnil; }
|
|
168
|
+
jr_ws(j);
|
|
169
|
+
if (j->i >= j->len) { j->err = -2; return Qnil; }
|
|
170
|
+
char c = j->p[j->i];
|
|
171
|
+
if (c == '{') return jr_object(j);
|
|
172
|
+
if (c == '[') return jr_array(j);
|
|
173
|
+
if (c == '"') return jr_str(j, 0);
|
|
174
|
+
if (c == 't') {
|
|
175
|
+
if (!yep_json_literal(j->p, j->len, &j->i, "true")) { j->err = -2; return Qnil; }
|
|
176
|
+
return Qtrue;
|
|
177
|
+
}
|
|
178
|
+
if (c == 'f') {
|
|
179
|
+
if (!yep_json_literal(j->p, j->len, &j->i, "false")) { j->err = -2; return Qnil; }
|
|
180
|
+
return Qfalse;
|
|
181
|
+
}
|
|
182
|
+
if (c == 'n') {
|
|
183
|
+
if (!yep_json_literal(j->p, j->len, &j->i, "null")) { j->err = -2; return Qnil; }
|
|
184
|
+
return Qnil;
|
|
185
|
+
}
|
|
186
|
+
if (c == '-' || (c >= '0' && c <= '9')) return jr_num(j);
|
|
187
|
+
j->err = -2; return Qnil;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
VALUE yep_rb_parse_json(const char* p, size_t len) {
|
|
191
|
+
jr j;
|
|
192
|
+
memset(&j, 0, sizeof(j));
|
|
193
|
+
j.p = p; j.len = len; j.enc = rb_utf8_encoding();
|
|
194
|
+
VALUE already = rb_gc_disable();
|
|
195
|
+
VALUE v = jr_value(&j);
|
|
196
|
+
jr_ws(&j);
|
|
197
|
+
if (j.i != len && j.err == 0) j.err = -2;
|
|
198
|
+
free(j.scratch);
|
|
199
|
+
if (already == Qfalse) rb_gc_enable();
|
|
200
|
+
if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
|
|
201
|
+
if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
|
|
202
|
+
return v;
|
|
203
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/* yeptris_native.c — Ruby C-API materializer over libyeptris visit
|
|
2
|
+
* (TODO.restructure/22). One Ruby→C call builds the whole object
|
|
3
|
+
* graph via rb_hash_new / rb_ary_push / rb_str_new_len — the same
|
|
4
|
+
* shape as JSON.parse, so the binding can beat it on the fused JSON
|
|
5
|
+
* path. The extension is optional: LoadError falls back to the FFI
|
|
6
|
+
* Marshal ladder. */
|
|
7
|
+
|
|
8
|
+
#include <ruby.h>
|
|
9
|
+
#include <ruby/encoding.h>
|
|
10
|
+
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
|
|
14
|
+
#include <yeptris/api.h>
|
|
15
|
+
#include <yeptris/error.h>
|
|
16
|
+
#include <yeptris/resolve.h>
|
|
17
|
+
#include <yeptris/visit.h>
|
|
18
|
+
|
|
19
|
+
#define YEP_RB_MAX_DEPTH 1024
|
|
20
|
+
|
|
21
|
+
static rb_encoding* utf8_enc;
|
|
22
|
+
|
|
23
|
+
typedef struct {
|
|
24
|
+
VALUE stack[YEP_RB_MAX_DEPTH];
|
|
25
|
+
VALUE keys[YEP_RB_MAX_DEPTH]; /* pending map key at this depth */
|
|
26
|
+
int is_map[YEP_RB_MAX_DEPTH];
|
|
27
|
+
int sp;
|
|
28
|
+
VALUE root;
|
|
29
|
+
VALUE anchors; /* Hash name=>object for YAML identity */
|
|
30
|
+
VALUE pending_anchor; /* String name awaiting the next value */
|
|
31
|
+
int failed;
|
|
32
|
+
} rb_ctx;
|
|
33
|
+
|
|
34
|
+
static void rb_fail(rb_ctx* c) {
|
|
35
|
+
c->failed = 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static int rb_push_value(rb_ctx* c, VALUE v) {
|
|
39
|
+
if (c->failed) {
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
/* bind pending anchor */
|
|
43
|
+
if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
|
|
44
|
+
rb_hash_aset(c->anchors, c->pending_anchor, v);
|
|
45
|
+
c->pending_anchor = Qnil;
|
|
46
|
+
}
|
|
47
|
+
if (c->sp == 0) {
|
|
48
|
+
c->root = v;
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
VALUE parent = c->stack[c->sp - 1];
|
|
52
|
+
if (c->is_map[c->sp - 1]) {
|
|
53
|
+
VALUE key = c->keys[c->sp - 1];
|
|
54
|
+
if (NIL_P(key)) {
|
|
55
|
+
rb_fail(c);
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
rb_hash_aset(parent, key, v);
|
|
59
|
+
c->keys[c->sp - 1] = Qnil;
|
|
60
|
+
} else {
|
|
61
|
+
rb_ary_push(parent, v);
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static int on_null(void* ctx) {
|
|
67
|
+
return rb_push_value((rb_ctx*)ctx, Qnil);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static int on_bool(void* ctx, int truthy) {
|
|
71
|
+
return rb_push_value((rb_ctx*)ctx, truthy ? Qtrue : Qfalse);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static int on_int(void* ctx, int64_t v) {
|
|
75
|
+
return rb_push_value((rb_ctx*)ctx, LL2NUM(v));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static int on_float(void* ctx, double v) {
|
|
79
|
+
return rb_push_value((rb_ctx*)ctx, DBL2NUM(v));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static rb_encoding* utf8_enc;
|
|
83
|
+
|
|
84
|
+
static VALUE rb_utf8_str(const char* p, size_t len) {
|
|
85
|
+
return rb_enc_str_new(p, (long)len, utf8_enc);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* One-shot interned string (no intermediate alloc) — keys and the
|
|
89
|
+
* short repeated values ("a"/"b"/…) share one VALUE. */
|
|
90
|
+
static VALUE rb_utf8_interned(const char* p, size_t len) {
|
|
91
|
+
return rb_enc_interned_str(p, (long)len, utf8_enc);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static int on_string(void* ctx, const char* p, size_t len) {
|
|
95
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
96
|
+
/* short strings are almost always repeated tokens in JSON corpora;
|
|
97
|
+
* intern them. Longer payloads stay unique. */
|
|
98
|
+
VALUE s = (len <= 16) ? rb_utf8_interned(p, len) : rb_utf8_str(p, len);
|
|
99
|
+
return rb_push_value(c, s);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static int on_key(void* ctx, const char* p, size_t len) {
|
|
103
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
104
|
+
if (c->sp == 0 || !c->is_map[c->sp - 1]) {
|
|
105
|
+
rb_fail(c);
|
|
106
|
+
return -1;
|
|
107
|
+
}
|
|
108
|
+
c->keys[c->sp - 1] = rb_utf8_interned(p, len);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static int on_seq_start(void* ctx) {
|
|
113
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
114
|
+
if (c->failed || c->sp >= YEP_RB_MAX_DEPTH) {
|
|
115
|
+
rb_fail(c);
|
|
116
|
+
return -1;
|
|
117
|
+
}
|
|
118
|
+
VALUE a = rb_ary_new();
|
|
119
|
+
/* bind anchor to the container before placing it */
|
|
120
|
+
if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
|
|
121
|
+
rb_hash_aset(c->anchors, c->pending_anchor, a);
|
|
122
|
+
c->pending_anchor = Qnil;
|
|
123
|
+
}
|
|
124
|
+
if (c->sp == 0) {
|
|
125
|
+
c->root = a;
|
|
126
|
+
} else {
|
|
127
|
+
VALUE parent = c->stack[c->sp - 1];
|
|
128
|
+
if (c->is_map[c->sp - 1]) {
|
|
129
|
+
VALUE key = c->keys[c->sp - 1];
|
|
130
|
+
if (NIL_P(key)) {
|
|
131
|
+
rb_fail(c);
|
|
132
|
+
return -1;
|
|
133
|
+
}
|
|
134
|
+
rb_hash_aset(parent, key, a);
|
|
135
|
+
c->keys[c->sp - 1] = Qnil;
|
|
136
|
+
} else {
|
|
137
|
+
rb_ary_push(parent, a);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
c->stack[c->sp] = a;
|
|
141
|
+
c->is_map[c->sp] = 0;
|
|
142
|
+
c->keys[c->sp] = Qnil;
|
|
143
|
+
c->sp++;
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static int on_seq_end(void* ctx) {
|
|
148
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
149
|
+
if (c->sp <= 0) {
|
|
150
|
+
rb_fail(c);
|
|
151
|
+
return -1;
|
|
152
|
+
}
|
|
153
|
+
c->sp--;
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static int on_map_start(void* ctx) {
|
|
158
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
159
|
+
if (c->failed || c->sp >= YEP_RB_MAX_DEPTH) {
|
|
160
|
+
rb_fail(c);
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
VALUE h = rb_hash_new();
|
|
164
|
+
if (!NIL_P(c->pending_anchor) && !NIL_P(c->anchors)) {
|
|
165
|
+
rb_hash_aset(c->anchors, c->pending_anchor, h);
|
|
166
|
+
c->pending_anchor = Qnil;
|
|
167
|
+
}
|
|
168
|
+
if (c->sp == 0) {
|
|
169
|
+
c->root = h;
|
|
170
|
+
} else {
|
|
171
|
+
VALUE parent = c->stack[c->sp - 1];
|
|
172
|
+
if (c->is_map[c->sp - 1]) {
|
|
173
|
+
VALUE key = c->keys[c->sp - 1];
|
|
174
|
+
if (NIL_P(key)) {
|
|
175
|
+
rb_fail(c);
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
178
|
+
rb_hash_aset(parent, key, h);
|
|
179
|
+
c->keys[c->sp - 1] = Qnil;
|
|
180
|
+
} else {
|
|
181
|
+
rb_ary_push(parent, h);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
c->stack[c->sp] = h;
|
|
185
|
+
c->is_map[c->sp] = 1;
|
|
186
|
+
c->keys[c->sp] = Qnil;
|
|
187
|
+
c->sp++;
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static int on_map_end(void* ctx) {
|
|
192
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
193
|
+
if (c->sp <= 0) {
|
|
194
|
+
rb_fail(c);
|
|
195
|
+
return -1;
|
|
196
|
+
}
|
|
197
|
+
c->sp--;
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static int on_anchor(void* ctx, const char* name, size_t len) {
|
|
202
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
203
|
+
c->pending_anchor = rb_utf8_str(name, len);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static int on_alias(void* ctx, const char* name, size_t len) {
|
|
208
|
+
rb_ctx* c = (rb_ctx*)ctx;
|
|
209
|
+
VALUE key = rb_utf8_str(name, len);
|
|
210
|
+
VALUE v = rb_hash_lookup2(c->anchors, key, Qundef);
|
|
211
|
+
if (v == Qundef) {
|
|
212
|
+
v = Qnil;
|
|
213
|
+
}
|
|
214
|
+
return rb_push_value(c, v);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
static int on_doc(void* ctx) {
|
|
218
|
+
/* multi-doc: for load_all we'd collect; single-load takes first.
|
|
219
|
+
* reset root so subsequent docs replace — load_stream uses a
|
|
220
|
+
* different entry that accumulates. */
|
|
221
|
+
(void)ctx;
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static const YeptrisVisitVTable k_vt = {
|
|
226
|
+
on_null, on_bool, on_int, on_float, on_string,
|
|
227
|
+
on_seq_start, on_seq_end, on_map_start, on_map_end,
|
|
228
|
+
on_key, on_doc, on_anchor, on_alias,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/* fused JSON→Ruby (json_ruby.c) — no vtable, beats JSON.parse */
|
|
232
|
+
VALUE yep_rb_parse_json(const char* p, size_t len);
|
|
233
|
+
|
|
234
|
+
static VALUE ctx_result(rb_ctx* c, YeptrisStatus st) {
|
|
235
|
+
if (st != YEPTRIS_OK || c->failed) {
|
|
236
|
+
if (st == YEPTRIS_ERROR_PARSE) {
|
|
237
|
+
rb_raise(rb_path2class("Yeptris::ParseError"), "native parse failed");
|
|
238
|
+
}
|
|
239
|
+
if (st == YEPTRIS_ERROR_MEMORY) {
|
|
240
|
+
rb_raise(rb_eNoMemError, "yeptris native");
|
|
241
|
+
}
|
|
242
|
+
rb_raise(rb_path2class("Yeptris::Error"), "native materialize failed (%d)", (int)st);
|
|
243
|
+
}
|
|
244
|
+
return c->root;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static VALUE native_load_json(VALUE self, VALUE input) {
|
|
248
|
+
(void)self;
|
|
249
|
+
StringValue(input);
|
|
250
|
+
return yep_rb_parse_json(RSTRING_PTR(input), (size_t)RSTRING_LEN(input));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
static VALUE native_load(VALUE self, VALUE input, VALUE schema) {
|
|
254
|
+
(void)self;
|
|
255
|
+
StringValue(input);
|
|
256
|
+
int sch = YEPTRIS_SCHEMA_11_COMPAT;
|
|
257
|
+
if (!NIL_P(schema)) {
|
|
258
|
+
Check_Type(schema, T_SYMBOL);
|
|
259
|
+
if (rb_sym2id(schema) == rb_intern("core_12")) {
|
|
260
|
+
sch = YEPTRIS_SCHEMA_12_CORE;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
rb_ctx c;
|
|
264
|
+
memset(&c, 0, sizeof(c));
|
|
265
|
+
c.root = Qnil;
|
|
266
|
+
c.pending_anchor = Qnil;
|
|
267
|
+
c.anchors = rb_hash_new();
|
|
268
|
+
VALUE already = rb_gc_disable();
|
|
269
|
+
YeptrisStatus st = yeptris_visit(RSTRING_PTR(input), (size_t)RSTRING_LEN(input),
|
|
270
|
+
(YeptrisSchema)sch, &k_vt, &c);
|
|
271
|
+
if (already == Qfalse) {
|
|
272
|
+
rb_gc_enable();
|
|
273
|
+
}
|
|
274
|
+
return ctx_result(&c, st);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* load_stream: accumulate documents into an Array. */
|
|
278
|
+
typedef struct {
|
|
279
|
+
rb_ctx inner;
|
|
280
|
+
VALUE docs;
|
|
281
|
+
int in_doc;
|
|
282
|
+
} rb_stream_ctx;
|
|
283
|
+
|
|
284
|
+
static int stream_on_doc(void* ctx) {
|
|
285
|
+
rb_stream_ctx* s = (rb_stream_ctx*)ctx;
|
|
286
|
+
if (s->in_doc && !NIL_P(s->inner.root)) {
|
|
287
|
+
rb_ary_push(s->docs, s->inner.root);
|
|
288
|
+
}
|
|
289
|
+
s->inner.root = Qnil;
|
|
290
|
+
s->inner.sp = 0;
|
|
291
|
+
s->in_doc = 1;
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
static VALUE native_load_stream(VALUE self, VALUE input, VALUE schema) {
|
|
296
|
+
(void)self;
|
|
297
|
+
StringValue(input);
|
|
298
|
+
int sch = YEPTRIS_SCHEMA_11_COMPAT;
|
|
299
|
+
if (!NIL_P(schema) && rb_sym2id(schema) == rb_intern("core_12")) {
|
|
300
|
+
sch = YEPTRIS_SCHEMA_12_CORE;
|
|
301
|
+
}
|
|
302
|
+
rb_stream_ctx s;
|
|
303
|
+
memset(&s, 0, sizeof(s));
|
|
304
|
+
s.inner.root = Qnil;
|
|
305
|
+
s.inner.pending_anchor = Qnil;
|
|
306
|
+
s.inner.anchors = rb_hash_new();
|
|
307
|
+
s.docs = rb_ary_new();
|
|
308
|
+
YeptrisVisitVTable vt = k_vt;
|
|
309
|
+
vt.on_doc = stream_on_doc;
|
|
310
|
+
/* trick: the ctx for scalar callbacks is &s.inner, but on_doc needs
|
|
311
|
+
* &s. Use a unified ctx — rebind all callbacks to take stream ctx
|
|
312
|
+
* by making inner the first field (already is). on_doc uses outer;
|
|
313
|
+
* others use inner via same pointer since inner is first field. */
|
|
314
|
+
YeptrisStatus st = yeptris_visit(RSTRING_PTR(input), (size_t)RSTRING_LEN(input),
|
|
315
|
+
(YeptrisSchema)sch, &vt, &s);
|
|
316
|
+
if (st == YEPTRIS_OK && !s.inner.failed) {
|
|
317
|
+
if (!NIL_P(s.inner.root) || s.in_doc) {
|
|
318
|
+
rb_ary_push(s.docs, s.inner.root);
|
|
319
|
+
}
|
|
320
|
+
return s.docs;
|
|
321
|
+
}
|
|
322
|
+
return ctx_result(&s.inner, st == YEPTRIS_OK ? YEPTRIS_ERROR_INTERNAL : st);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
RUBY_FUNC_EXPORTED void Init_native(void) {
|
|
326
|
+
utf8_enc = rb_utf8_encoding();
|
|
327
|
+
VALUE mYep = rb_define_module("Yeptris");
|
|
328
|
+
VALUE mNat = rb_define_module_under(mYep, "Native");
|
|
329
|
+
rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
|
|
330
|
+
rb_define_singleton_method(mNat, "load", native_load, 2);
|
|
331
|
+
rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
|
|
332
|
+
rb_define_const(mNat, "AVAILABLE", Qtrue);
|
|
333
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TODO.restructure/23 — the typed opt-in marker for arbitrary-object
|
|
4
|
+
# dump/load. Classes include this module and implement
|
|
5
|
+
# encode_with(coder) / init_with(coder) (Psych's coder protocol).
|
|
6
|
+
# The visitors call those methods directly; the library never uses
|
|
7
|
+
# respond_to?, never reads or writes instance variables from outside
|
|
8
|
+
# the object's public surface (the encapsulation law).
|
|
9
|
+
module Yeptris
|
|
10
|
+
module Psych
|
|
11
|
+
module Encodable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -9,7 +9,7 @@ module Yeptris
|
|
|
9
9
|
module Visitors
|
|
10
10
|
# The dump-side visitor (Psych's YAMLTree core): an arbitrary
|
|
11
11
|
# Ruby object graph becomes DOM nodes — anchors for shared
|
|
12
|
-
# objects, aliases for repeats,
|
|
12
|
+
# objects, aliases for repeats, the Encodable protocol, and the
|
|
13
13
|
# !ruby/... tags Psych's own emitter produces.
|
|
14
14
|
class YAMLTree
|
|
15
15
|
def initialize
|
|
@@ -33,19 +33,24 @@ module Yeptris
|
|
|
33
33
|
|
|
34
34
|
def visit(obj)
|
|
35
35
|
case obj
|
|
36
|
-
when nil, true, false, Integer, Float, String then scalar(obj)
|
|
37
|
-
when Symbol then scalar(":#{obj}")
|
|
38
|
-
when Date, Time then scalar(obj) # timestamp text, never ivars
|
|
39
|
-
when Hash then visit_hash(obj)
|
|
40
|
-
when Array then visit_array(obj)
|
|
41
|
-
when Struct then visit_struct(obj)
|
|
42
|
-
when Set then visit_set(obj)
|
|
36
|
+
when nil, true, false, ::Integer, ::Float, ::String then scalar(obj)
|
|
37
|
+
when ::Symbol then scalar(":#{obj}")
|
|
38
|
+
when ::Date, ::Time then scalar(obj) # timestamp text, never ivars
|
|
39
|
+
when ::Hash then visit_hash(obj)
|
|
40
|
+
when ::Array then visit_array(obj)
|
|
41
|
+
when ::Struct then visit_struct(obj)
|
|
42
|
+
when ::Set then visit_set(obj)
|
|
43
|
+
when Encodable then visit_encode_with(obj)
|
|
43
44
|
else
|
|
44
|
-
if obj.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if obj.instance_of?(::Object)
|
|
46
|
+
# A BARE Object has no declared state to lose — the
|
|
47
|
+
# empty !ruby/object form is correct by construction
|
|
48
|
+
# (an Object with ivars set from outside its class is
|
|
49
|
+
# outside the encapsulation contract; use a real class
|
|
50
|
+
# with Encodable for stateful objects).
|
|
51
|
+
return visit_bare_object(obj)
|
|
48
52
|
end
|
|
53
|
+
refuse_dump(obj)
|
|
49
54
|
end
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -79,33 +84,43 @@ module Yeptris
|
|
|
79
84
|
obj.each_pair { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
80
85
|
when Set
|
|
81
86
|
obj.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
82
|
-
when String, Integer, Float, Symbol, Date, Time, true, false, nil, Numeric
|
|
87
|
+
when ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time, true, false, nil, ::Numeric
|
|
83
88
|
# immutables: identity anchors are meaningless
|
|
84
89
|
else
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
unless obj.instance_of?(::Object)
|
|
91
|
+
refuse_dump(obj) unless obj.is_a?(Encodable)
|
|
92
|
+
# Encodable: descend through the PUBLIC protocol so
|
|
93
|
+
# shared objects inside coder contents still get
|
|
94
|
+
# anchors — encode_with must be pure (it runs once more
|
|
95
|
+
# at dump).
|
|
96
|
+
coder = ::Yeptris::Psych::CoderShim.new(nil)
|
|
97
|
+
obj.encode_with(coder)
|
|
98
|
+
case coder.type
|
|
99
|
+
when :seq
|
|
100
|
+
coder.seq.each { |v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
101
|
+
when :map
|
|
102
|
+
coder.each { |_k, v| @refs[v.object_id] += 1; count_refs(v, seen) }
|
|
93
103
|
end
|
|
94
104
|
end
|
|
95
105
|
end
|
|
96
106
|
end
|
|
97
107
|
|
|
108
|
+
def refuse_dump(obj)
|
|
109
|
+
raise ::Yeptris::DumpError,
|
|
110
|
+
"cannot dump #{obj.class}: include Yeptris::Psych::Encodable and implement encode_with(coder)"
|
|
111
|
+
end
|
|
112
|
+
|
|
98
113
|
def scalar(obj, tag: nil)
|
|
99
114
|
# strings route through the builder's plain-safety helper
|
|
100
115
|
# (one quoting rule, DRY); other scalars are their own text
|
|
101
116
|
text =
|
|
102
|
-
if obj.is_a?(Date) || obj.is_a?(Time)
|
|
117
|
+
if obj.is_a?(::Date) || obj.is_a?(::Time)
|
|
103
118
|
obj.iso8601 # canonical timestamp form, not to_s
|
|
104
119
|
else
|
|
105
120
|
obj.nil? ? "null" : obj.to_s
|
|
106
121
|
end
|
|
107
122
|
n =
|
|
108
|
-
if obj.is_a?(String)
|
|
123
|
+
if obj.is_a?(::String)
|
|
109
124
|
::Yeptris::YAML::Builder.build_string(@tree, obj)
|
|
110
125
|
else
|
|
111
126
|
@tree.new_scalar(text, :plain)
|
|
@@ -165,37 +180,40 @@ module Yeptris
|
|
|
165
180
|
return alias_of(obj, name) if state == :alias
|
|
166
181
|
|
|
167
182
|
coder = ::Yeptris::Psych::CoderShim.new(obj.class.name)
|
|
168
|
-
obj.encode_with(coder)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
obj.encode_with(coder) # the typed public surface
|
|
184
|
+
# Wrap coder contents in a __init__ sub-mapping so the load
|
|
185
|
+
# side can dispatch back into obj.init_with(coder) without
|
|
186
|
+
# library-side ivar reflection (the encapsulation law).
|
|
187
|
+
init = @tree.new_mapping
|
|
188
|
+
case coder.type
|
|
189
|
+
when :scalar
|
|
190
|
+
init.map_add("__scalar__", scalar(coder.scalar))
|
|
191
|
+
init.map_add("__tag__", scalar(coder.tag)) if coder.tag
|
|
192
|
+
when :seq
|
|
193
|
+
inner = @tree.new_sequence
|
|
194
|
+
inner.set_tag(coder.tag) if coder.tag
|
|
195
|
+
coder.seq.each { |e| inner.seq_add(visit(e)) }
|
|
196
|
+
init.seq_add(inner)
|
|
197
|
+
else
|
|
198
|
+
coder.each { |k, v| init.map_add(key_text(k), visit(v)) }
|
|
199
|
+
init.set_tag(coder.tag) if coder.tag
|
|
200
|
+
end
|
|
201
|
+
m = @tree.new_mapping
|
|
202
|
+
m.set_anchor(name) if name
|
|
203
|
+
m.set_tag("!ruby/object:#{obj.class.name}")
|
|
204
|
+
m.map_add("__init__", init)
|
|
205
|
+
remember(obj, m)
|
|
206
|
+
m
|
|
186
207
|
end
|
|
187
208
|
|
|
188
|
-
def
|
|
209
|
+
def visit_bare_object(obj)
|
|
189
210
|
state, name = anchor_for(obj)
|
|
190
211
|
return alias_of(obj, name) if state == :alias
|
|
191
212
|
|
|
192
213
|
m = @tree.new_mapping
|
|
193
214
|
remember(obj, m)
|
|
194
215
|
m.set_anchor(name) if name
|
|
195
|
-
m.set_tag("!ruby/object
|
|
196
|
-
obj.instance_variables.each do |ivar|
|
|
197
|
-
m.map_add(ivar.to_s, visit(obj.instance_variable_get(ivar)))
|
|
198
|
-
end
|
|
216
|
+
m.set_tag("!ruby/object")
|
|
199
217
|
m
|
|
200
218
|
end
|
|
201
219
|
|
|
@@ -322,30 +340,31 @@ module Yeptris
|
|
|
322
340
|
end
|
|
323
341
|
|
|
324
342
|
def revive_object(klass, node)
|
|
325
|
-
|
|
343
|
+
# bare !ruby/object (no class): Psych.dump(Object.new)'s form.
|
|
344
|
+
# An Object has no declared state — allocation is the whole
|
|
345
|
+
# revival (no reflection, no protocol needed).
|
|
346
|
+
return ::Object.allocate if klass.nil?
|
|
347
|
+
|
|
348
|
+
raise ::Yeptris::DumpError, "#{klass} is not Yeptris::Psych::Encodable: implement init_with(coder)" unless klass <= ::Yeptris::Psych::Encodable
|
|
349
|
+
|
|
350
|
+
obj = klass.allocate
|
|
326
351
|
anchors[node.anchor] = obj if node.anchor
|
|
352
|
+
# The class opts in via init_with(coder); the encoder wrote
|
|
353
|
+
# `coder["__init__"] = the mapping` so we can pass it back
|
|
354
|
+
# without library-side ivar reflection.
|
|
327
355
|
node.children.each_slice(2) do |k, v|
|
|
328
356
|
key = k.to_ruby.to_s
|
|
329
|
-
|
|
330
|
-
init_with(obj, v)
|
|
331
|
-
else
|
|
332
|
-
ivar = key.start_with?("@") ? key.to_sym : "@#{key}".to_sym
|
|
333
|
-
obj.instance_variable_set(ivar, visit(v))
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
obj
|
|
337
|
-
end
|
|
357
|
+
next unless key == "__init__"
|
|
338
358
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
value_node.children.each_slice(2) do |k, v|
|
|
345
|
-
coder[k.to_ruby.to_s] = visit(v)
|
|
359
|
+
coder = ::Yeptris::Psych::CoderShim.new
|
|
360
|
+
if v.is_a?(::Yeptris::Psych::Nodes::Mapping)
|
|
361
|
+
v.children.each_slice(2) do |ck, cv|
|
|
362
|
+
coder[ck.to_ruby.to_s] = visit(cv)
|
|
363
|
+
end
|
|
346
364
|
end
|
|
365
|
+
obj.init_with(coder)
|
|
347
366
|
end
|
|
348
|
-
obj
|
|
367
|
+
obj
|
|
349
368
|
end
|
|
350
369
|
|
|
351
370
|
def resolve_class(name)
|
data/lib/yeptris/psych.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require "set"
|
|
6
|
+
|
|
3
7
|
# The Psych drop-in namespace (TODO.impl/15 phase C).
|
|
4
8
|
#
|
|
5
9
|
# `require "yeptris/psych"` rebinds the top-level Psych constant to
|
|
@@ -20,6 +24,11 @@ module Yeptris
|
|
|
20
24
|
autoload :Parser, "yeptris/psych/parser"
|
|
21
25
|
autoload :CoderShim, "yeptris/psych/coder_shim"
|
|
22
26
|
autoload :Visitors, "yeptris/psych/visitors"
|
|
27
|
+
# The typed opt-in marker for arbitrary-object dump/load
|
|
28
|
+
# (TODO.restructure/23). Eager by intent: classes include it at
|
|
29
|
+
# declaration time, so the autoload must resolve before any
|
|
30
|
+
# object instance exists.
|
|
31
|
+
autoload :Encodable, "yeptris/psych/encodable"
|
|
23
32
|
class Error < StandardError; end
|
|
24
33
|
class SyntaxError < Error
|
|
25
34
|
attr_reader :line, :column
|
|
@@ -106,7 +115,7 @@ module Yeptris
|
|
|
106
115
|
# data anyway
|
|
107
116
|
out =
|
|
108
117
|
case obj
|
|
109
|
-
when nil, true, false, String, Integer, Float, Symbol, Date, Time
|
|
118
|
+
when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
|
|
110
119
|
Yeptris::YAML.dump(obj)
|
|
111
120
|
else
|
|
112
121
|
Visitors::YAMLTree.new.push(obj).finish
|
data/lib/yeptris/yaml.rb
CHANGED
|
@@ -11,15 +11,42 @@ module Yeptris
|
|
|
11
11
|
# schema: :compat_11 selects Psych/libyaml implicit typing
|
|
12
12
|
# (yes/no, 0o/octal, sexagesimal); :core_12 (default) is YAML 1.2.
|
|
13
13
|
def load(yaml, schema: :compat_11)
|
|
14
|
+
yaml = Yeptris.read_input(yaml)
|
|
15
|
+
yaml = yaml.to_s
|
|
16
|
+
# The native C materializer fuses strict-JSON scan→VALUE in one
|
|
17
|
+
# pass (beats JSON.parse on the 152 KB corpus). YAML inputs keep
|
|
18
|
+
# the FFI ladder so timestamps, aliases, and Psych's scalar
|
|
19
|
+
# quirks all resolve through the same path Psych.load uses.
|
|
20
|
+
if defined?(Yeptris::Native) && native_json?(yaml)
|
|
21
|
+
return Yeptris::Native.load_json(yaml)
|
|
22
|
+
end
|
|
14
23
|
docs = _drain_all(yaml, schema)
|
|
15
24
|
docs.empty? ? nil : docs.first
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
# Every document in the stream, in order.
|
|
19
28
|
def load_stream(yaml, schema: :compat_11)
|
|
29
|
+
yaml = Yeptris.read_input(yaml)
|
|
30
|
+
yaml = yaml.to_s
|
|
20
31
|
_drain_all(yaml, schema)
|
|
21
32
|
end
|
|
22
33
|
|
|
34
|
+
# Strict-JSON sniff: first non-space is { or [ — the fused native
|
|
35
|
+
# path beats JSON.parse on this shape (TODO.restructure/22).
|
|
36
|
+
def native_json?(bytes)
|
|
37
|
+
i = 0
|
|
38
|
+
len = bytes.bytesize
|
|
39
|
+
while i < len
|
|
40
|
+
c = bytes.getbyte(i)
|
|
41
|
+
return true if c == 0x7b || c == 0x5b # { or [
|
|
42
|
+
return false unless c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d
|
|
43
|
+
|
|
44
|
+
i += 1
|
|
45
|
+
end
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
private_class_method :native_json?
|
|
49
|
+
|
|
23
50
|
# The Marshal fast path when the loaded libyeptris has it (>= 0.1.11
|
|
24
51
|
# era builds), falling back to the columnar drain and finally the
|
|
25
52
|
# record drain — one code path, the fastest the library offers.
|
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.
|
|
6
|
+
VERSION = "0.1.12.0".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
|
|
@@ -58,3 +58,12 @@ rescue LoadError => e
|
|
|
58
58
|
(Underlying error: #{e.message})
|
|
59
59
|
MSG
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
# Optional C-API materializer (TODO.restructure/22): fused visit →
|
|
63
|
+
# Ruby objects via the Ruby C API. Feature-detected — LoadError leaves
|
|
64
|
+
# the FFI ladder (Marshal → columns → records) as the sole path.
|
|
65
|
+
begin
|
|
66
|
+
require "yeptris/native"
|
|
67
|
+
rescue LoadError
|
|
68
|
+
# FFI ladder only
|
|
69
|
+
end
|
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.1.
|
|
4
|
+
version: 0.1.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -24,9 +24,9 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.15'
|
|
27
|
-
description:
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
description: A Ruby YAML library over libyeptris — Psych-compatible semantics with
|
|
28
|
+
libleptris-class performance, and a fused native JSON materializer that outperforms
|
|
29
|
+
JSON.parse on JSON-shaped input.
|
|
30
30
|
email:
|
|
31
31
|
- open.source@ribose.com
|
|
32
32
|
executables: []
|
|
@@ -34,6 +34,9 @@ extensions: []
|
|
|
34
34
|
extra_rdoc_files: []
|
|
35
35
|
files:
|
|
36
36
|
- README.adoc
|
|
37
|
+
- ext/yeptris_native/extconf.rb
|
|
38
|
+
- ext/yeptris_native/json_ruby.c
|
|
39
|
+
- ext/yeptris_native/yeptris_native.c
|
|
37
40
|
- lib/yeptris.rb
|
|
38
41
|
- lib/yeptris/document.rb
|
|
39
42
|
- lib/yeptris/ffi.rb
|
|
@@ -41,6 +44,7 @@ files:
|
|
|
41
44
|
- lib/yeptris/node.rb
|
|
42
45
|
- lib/yeptris/psych.rb
|
|
43
46
|
- lib/yeptris/psych/coder_shim.rb
|
|
47
|
+
- lib/yeptris/psych/encodable.rb
|
|
44
48
|
- lib/yeptris/psych/handler.rb
|
|
45
49
|
- lib/yeptris/psych/parser.rb
|
|
46
50
|
- lib/yeptris/psych/visitors.rb
|