yeptris 0.6.18.2-arm-linux-eabihf
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/README.adoc +141 -0
- data/ext/build_windows_native.rb +41 -0
- data/ext/libyeptris/extconf.rb +57 -0
- data/ext/yeptris_native/cbor_ruby.c +193 -0
- data/ext/yeptris_native/extconf.rb +47 -0
- data/ext/yeptris_native/json_ruby.c +354 -0
- data/ext/yeptris_native/yeptris_native.c +497 -0
- data/lib/yeptris/cbor.rb +130 -0
- data/lib/yeptris/document.rb +255 -0
- data/lib/yeptris/ffi.rb +426 -0
- data/lib/yeptris/json/descriptor.rb +266 -0
- data/lib/yeptris/json.rb +394 -0
- data/lib/yeptris/materializer.rb +354 -0
- data/lib/yeptris/native-3.4.so +0 -0
- data/lib/yeptris/node.rb +453 -0
- data/lib/yeptris/psych/class_loader.rb +24 -0
- data/lib/yeptris/psych/coder_shim.rb +55 -0
- data/lib/yeptris/psych/drop_in.rb +63 -0
- data/lib/yeptris/psych/encodable.rb +14 -0
- data/lib/yeptris/psych/handler.rb +90 -0
- data/lib/yeptris/psych/parser.rb +105 -0
- data/lib/yeptris/psych/scalar_scanner.rb +149 -0
- data/lib/yeptris/psych/visitors.rb +545 -0
- data/lib/yeptris/psych.rb +622 -0
- data/lib/yeptris/schema.rb +253 -0
- data/lib/yeptris/valueml.rb +455 -0
- data/lib/yeptris/yaml/descriptor.rb +56 -0
- data/lib/yeptris/yaml.rb +422 -0
- data/lib/yeptris.rb +113 -0
- data/libyeptris.so +0 -0
- data/vendor/libyeptris/CMakeLists.txt +254 -0
- data/vendor/libyeptris/cmake/yeptris-config.cmake.in +5 -0
- data/vendor/libyeptris/cmake/yeptris.pc.in +11 -0
- data/vendor/libyeptris/src/CMakeLists.txt +167 -0
- data/vendor/libyeptris/src/include/yeptris/api.h +32 -0
- data/vendor/libyeptris/src/include/yeptris/cbor.h +101 -0
- data/vendor/libyeptris/src/include/yeptris/dom.h +190 -0
- data/vendor/libyeptris/src/include/yeptris/emit.h +81 -0
- data/vendor/libyeptris/src/include/yeptris/error.h +41 -0
- data/vendor/libyeptris/src/include/yeptris/events.h +157 -0
- data/vendor/libyeptris/src/include/yeptris/json.h +45 -0
- data/vendor/libyeptris/src/include/yeptris/json.hpp +258 -0
- data/vendor/libyeptris/src/include/yeptris/jsonc_compat.h +115 -0
- data/vendor/libyeptris/src/include/yeptris/marshal.h +52 -0
- data/vendor/libyeptris/src/include/yeptris/parse.h +48 -0
- data/vendor/libyeptris/src/include/yeptris/plan.h +100 -0
- data/vendor/libyeptris/src/include/yeptris/resolve.h +71 -0
- data/vendor/libyeptris/src/include/yeptris/schema.h +121 -0
- data/vendor/libyeptris/src/include/yeptris/tape.h +123 -0
- data/vendor/libyeptris/src/include/yeptris/types.h +38 -0
- data/vendor/libyeptris/src/include/yeptris/values.h +102 -0
- data/vendor/libyeptris/src/include/yeptris/version.h.in +29 -0
- data/vendor/libyeptris/src/include/yeptris/visit.h +77 -0
- data/vendor/libyeptris/src/include/yeptris/yajl_compat.h +135 -0
- data/vendor/libyeptris/src/include/yeptris.h +23 -0
- data/vendor/libyeptris/src/yeptris/build.c +376 -0
- data/vendor/libyeptris/src/yeptris/cbor/cbor.h +25 -0
- data/vendor/libyeptris/src/yeptris/cbor/decode.c +981 -0
- data/vendor/libyeptris/src/yeptris/cbor/encode.c +835 -0
- data/vendor/libyeptris/src/yeptris/cbor/sink.h +58 -0
- data/vendor/libyeptris/src/yeptris/common/chartype.c +41 -0
- data/vendor/libyeptris/src/yeptris/common/chartype.h +77 -0
- data/vendor/libyeptris/src/yeptris/common/cpu.c +81 -0
- data/vendor/libyeptris/src/yeptris/common/cpu.h +40 -0
- data/vendor/libyeptris/src/yeptris/common/error.c +65 -0
- data/vendor/libyeptris/src/yeptris/common/error.h +79 -0
- data/vendor/libyeptris/src/yeptris/common/mutex.h +45 -0
- data/vendor/libyeptris/src/yeptris/common/nametab.c +275 -0
- data/vendor/libyeptris/src/yeptris/common/nametab.h +68 -0
- data/vendor/libyeptris/src/yeptris/common/port.h +74 -0
- data/vendor/libyeptris/src/yeptris/common/simd_text.c +49 -0
- data/vendor/libyeptris/src/yeptris/common/simd_text.h +295 -0
- data/vendor/libyeptris/src/yeptris/common/simd_text_avx2.c +534 -0
- data/vendor/libyeptris/src/yeptris/common/simd_text_neon.c +662 -0
- data/vendor/libyeptris/src/yeptris/common/simd_text_scalar.c +298 -0
- data/vendor/libyeptris/src/yeptris/common/string_view.c +34 -0
- data/vendor/libyeptris/src/yeptris/common/string_view.h +93 -0
- data/vendor/libyeptris/src/yeptris/doc.h +56 -0
- data/vendor/libyeptris/src/yeptris/dom/dom.c +1192 -0
- data/vendor/libyeptris/src/yeptris/dom/dom.h +312 -0
- data/vendor/libyeptris/src/yeptris/dom/hpool.c +107 -0
- data/vendor/libyeptris/src/yeptris/dom/mapindex.c +173 -0
- data/vendor/libyeptris/src/yeptris/dom/mapindex.h +61 -0
- data/vendor/libyeptris/src/yeptris/dom/mutate.c +431 -0
- data/vendor/libyeptris/src/yeptris/emit/emit.c +434 -0
- data/vendor/libyeptris/src/yeptris/emit/float/api.h +38 -0
- data/vendor/libyeptris/src/yeptris/emit/float/dragon.c +405 -0
- data/vendor/libyeptris/src/yeptris/emit/float/floatint.h +231 -0
- data/vendor/libyeptris/src/yeptris/emit/float/print.c +396 -0
- data/vendor/libyeptris/src/yeptris/emit/style.c +81 -0
- data/vendor/libyeptris/src/yeptris/emit/style.h +24 -0
- data/vendor/libyeptris/src/yeptris/emit/writer.c +816 -0
- data/vendor/libyeptris/src/yeptris/emit/writer.h +76 -0
- data/vendor/libyeptris/src/yeptris/encoding/bom.c +31 -0
- data/vendor/libyeptris/src/yeptris/encoding/encoding.h +63 -0
- data/vendor/libyeptris/src/yeptris/encoding/transcode.c +148 -0
- data/vendor/libyeptris/src/yeptris/encoding/utf8_validate.c +178 -0
- data/vendor/libyeptris/src/yeptris/events/capture.c +150 -0
- data/vendor/libyeptris/src/yeptris/events/capture.h +39 -0
- data/vendor/libyeptris/src/yeptris/events/iterparse.c +208 -0
- data/vendor/libyeptris/src/yeptris/events/pull.c +97 -0
- data/vendor/libyeptris/src/yeptris/events/push.c +104 -0
- data/vendor/libyeptris/src/yeptris/events/recorder.c +107 -0
- data/vendor/libyeptris/src/yeptris/events/values.c +527 -0
- data/vendor/libyeptris/src/yeptris/events/values_priv.h +51 -0
- data/vendor/libyeptris/src/yeptris/events/yaml_compat.c +86 -0
- data/vendor/libyeptris/src/yeptris/events/yaml_compat.h +90 -0
- data/vendor/libyeptris/src/yeptris/jsonapi/jsonc_compat.c +687 -0
- data/vendor/libyeptris/src/yeptris/jsonapi/yajl_compat.c +728 -0
- data/vendor/libyeptris/src/yeptris/marshal.c +782 -0
- data/vendor/libyeptris/src/yeptris/memory/allocator.c +20 -0
- data/vendor/libyeptris/src/yeptris/memory/allocator.h +43 -0
- data/vendor/libyeptris/src/yeptris/memory/arena.c +134 -0
- data/vendor/libyeptris/src/yeptris/memory/arena.h +52 -0
- data/vendor/libyeptris/src/yeptris/memory/pool.c +122 -0
- data/vendor/libyeptris/src/yeptris/memory/pool.h +36 -0
- data/vendor/libyeptris/src/yeptris/parse/engine.c +3698 -0
- data/vendor/libyeptris/src/yeptris/parse/engine.h +65 -0
- data/vendor/libyeptris/src/yeptris/parse/events.h +124 -0
- data/vendor/libyeptris/src/yeptris/parse/numbers.c +349 -0
- data/vendor/libyeptris/src/yeptris/parse/numbers.h +33 -0
- data/vendor/libyeptris/src/yeptris/parse/scalars.c +416 -0
- data/vendor/libyeptris/src/yeptris/parse/scalars.h +75 -0
- data/vendor/libyeptris/src/yeptris/parse.c +737 -0
- data/vendor/libyeptris/src/yeptris/plan.c +824 -0
- data/vendor/libyeptris/src/yeptris/resolve/compat11.c +334 -0
- data/vendor/libyeptris/src/yeptris/resolve/core12.c +143 -0
- data/vendor/libyeptris/src/yeptris/resolve/resolver.h +56 -0
- data/vendor/libyeptris/src/yeptris/resolve/tags.c +58 -0
- data/vendor/libyeptris/src/yeptris/scan/json.c +792 -0
- data/vendor/libyeptris/src/yeptris/scan/json.h +109 -0
- data/vendor/libyeptris/src/yeptris/scan/scan.c +463 -0
- data/vendor/libyeptris/src/yeptris/scan/scan.h +150 -0
- data/vendor/libyeptris/src/yeptris/schema.c +308 -0
- data/vendor/libyeptris/src/yeptris/tape.c +1308 -0
- data/vendor/libyeptris/src/yeptris/tape_in.h +19 -0
- data/vendor/libyeptris/src/yeptris/version.c +7 -0
- data/vendor/libyeptris/src/yeptris/visit/dom_visit.c +115 -0
- data/vendor/libyeptris/src/yeptris/visit/json_visit.c +293 -0
- data/vendor/libyeptris/src/yeptris/visit/yaml_visit.c +127 -0
- metadata +199 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The Descriptor plan walk (yeptris#293 slice two, over the C plan
|
|
4
|
+
# ABI of libyeptris v0.6.3): compile a row shape once, then apply it
|
|
5
|
+
# to a JSON document in ONE native pass producing typed COLUMNS —
|
|
6
|
+
# the engine never materializes the intermediate Ruby Hash/Array
|
|
7
|
+
# tree (lutaml-model's hydration reads columns, not trees).
|
|
8
|
+
#
|
|
9
|
+
# descriptor = Yeptris::JSON::Descriptor.build(
|
|
10
|
+
# kind: :seq, # or :map with path: — the rows container
|
|
11
|
+
# children: [
|
|
12
|
+
# { name: "id", kind: :int },
|
|
13
|
+
# { name: "name", kind: :str },
|
|
14
|
+
# ])
|
|
15
|
+
# result = descriptor.walk(json)
|
|
16
|
+
# result.column("id") # => [1, 2, ...] (nil where null/missing)
|
|
17
|
+
# result.to_a # => [{ "id" => 1, "name" => "x" }, ...]
|
|
18
|
+
#
|
|
19
|
+
# Vocabulary alignment with Leptris::XML::Descriptor (the shape
|
|
20
|
+
# vocabulary lutaml-model shares across engines): :collection at the
|
|
21
|
+
# root is the rows container (the seq form), :scalar is the string
|
|
22
|
+
# leaf. Slice one covers rows of scalar leaves — nested plans, the
|
|
23
|
+
# YAML leg, and partial descriptors ride the board item
|
|
24
|
+
# (TODO.restructure/87).
|
|
25
|
+
class Yeptris::JSON::Descriptor
|
|
26
|
+
class Error < ::Yeptris::JSON::Error; end
|
|
27
|
+
|
|
28
|
+
# spec leaf kinds → the C column kind codes (plan.h)
|
|
29
|
+
LEAF_KINDS = {
|
|
30
|
+
int: 0,
|
|
31
|
+
float: 1,
|
|
32
|
+
str: 2,
|
|
33
|
+
bool: 3,
|
|
34
|
+
scalar: 2, # leptris's string-value row kind
|
|
35
|
+
}.freeze
|
|
36
|
+
private_constant :LEAF_KINDS
|
|
37
|
+
|
|
38
|
+
# root forms: the rows container's shape (:collection is leptris's
|
|
39
|
+
# name for a repeated-rows container)
|
|
40
|
+
ROOT_KINDS = %i[seq map collection].freeze
|
|
41
|
+
private_constant :ROOT_KINDS
|
|
42
|
+
|
|
43
|
+
LEAF_SPEC_NAMES = %w[int float str bool].freeze
|
|
44
|
+
private_constant :LEAF_SPEC_NAMES
|
|
45
|
+
|
|
46
|
+
class Handle < ::FFI::AutoPointer
|
|
47
|
+
def self.release(ptr)
|
|
48
|
+
::Yeptris::FFI.yeptris_plan_free(ptr)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# +kind+: :seq (rows are the root array) or :map (rows live under
|
|
53
|
+
# +path:+'s value). +path+: a String, or an Array of Strings for a
|
|
54
|
+
# segmented (nested) path. +children+: the leaf columns; each row
|
|
55
|
+
# is a mapping, every child names one typed column.
|
|
56
|
+
def self.build(kind:, path: nil, children:)
|
|
57
|
+
handle, names = compile_handle(kind, path, children)
|
|
58
|
+
new(handle, names)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The compile half of .build (shared with the YAML subclass's
|
|
62
|
+
# build — ONE owner of the spec grammar). Returns the owned handle
|
|
63
|
+
# and the leaf names.
|
|
64
|
+
def self.compile_handle(kind, path, children)
|
|
65
|
+
unless ROOT_KINDS.include?(kind)
|
|
66
|
+
raise ArgumentError, "kind must be one of #{ROOT_KINDS.inspect}, got #{kind.inspect}"
|
|
67
|
+
end
|
|
68
|
+
unless children.is_a?(::Array) && !children.empty?
|
|
69
|
+
raise ArgumentError, "children must be a non-empty Array"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
leaves = children.map do |child|
|
|
73
|
+
name = child[:name]
|
|
74
|
+
code = LEAF_KINDS[child[:kind]]
|
|
75
|
+
if code.nil?
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"leaf kind must be one of #{LEAF_KINDS.keys.inspect}, got #{child[:kind].inspect}"
|
|
78
|
+
end
|
|
79
|
+
unless name.is_a?(::String) && !name.empty?
|
|
80
|
+
raise ArgumentError, "leaf name must be a non-empty String, got #{name.inspect}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
{ "name" => name, "kind" => LEAF_SPEC_NAMES[code] }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
case path
|
|
87
|
+
when nil then nil
|
|
88
|
+
when ::String then spec_path = path
|
|
89
|
+
when ::Array
|
|
90
|
+
unless path.all? { |seg| seg.is_a?(::String) && !seg.empty? }
|
|
91
|
+
raise ArgumentError, "path segments must be non-empty Strings"
|
|
92
|
+
end
|
|
93
|
+
spec_path = path
|
|
94
|
+
else
|
|
95
|
+
raise ArgumentError, "path must be a String or an Array of Strings"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
spec = { "kind" => kind == :map ? "map" : "seq", "children" => leaves }
|
|
99
|
+
spec["path"] = spec_path unless spec_path.nil?
|
|
100
|
+
|
|
101
|
+
# the spec is strict JSON (the C compiler parses it through the
|
|
102
|
+
# strict JSON DOM); compile once, the engine owns the copy
|
|
103
|
+
spec_json = ::JSON.generate(spec)
|
|
104
|
+
st = ::FFI::MemoryPointer.new(:int)
|
|
105
|
+
raw = ::Yeptris::FFI.yeptris_plan_compile(spec_json, spec_json.bytesize, st)
|
|
106
|
+
if raw.null?
|
|
107
|
+
raise Error, "plan compile failed (status=#{st.read_int}): #{spec_json}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
[Handle.new(raw), leaves.map { |leaf| leaf["name"] }]
|
|
111
|
+
end
|
|
112
|
+
private_class_method :compile_handle
|
|
113
|
+
|
|
114
|
+
attr_reader :names # the planned leaf names, in spec order
|
|
115
|
+
|
|
116
|
+
# @api private — handles come from .build
|
|
117
|
+
def initialize(handle, names)
|
|
118
|
+
@handle = handle
|
|
119
|
+
@names = names.dup.freeze
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Applies the plan to +json+ (String or IO): parse the tape, one C
|
|
123
|
+
# plan pass, bulk-read the typed columns. The returned PlanResult
|
|
124
|
+
# is standalone Ruby (nothing borrowed from the document).
|
|
125
|
+
def walk(json)
|
|
126
|
+
src = ::Yeptris.read_input(json).to_s
|
|
127
|
+
tape = ::Yeptris::FFI::JsonTape.new
|
|
128
|
+
rc = ::Yeptris::FFI.yeptris_parse_json_tape(src, src.bytesize, tape)
|
|
129
|
+
raise ::Yeptris::JSON::ParseError, ::Yeptris::FFI.last_error_message if rc != ::Yeptris::FFI::OK
|
|
130
|
+
|
|
131
|
+
begin
|
|
132
|
+
st = ::FFI::MemoryPointer.new(:int)
|
|
133
|
+
raw = ::Yeptris::FFI.yeptris_tape_plan_walk(tape, @handle, st)
|
|
134
|
+
if raw.null?
|
|
135
|
+
raise Error,
|
|
136
|
+
"plan walk failed (status=#{st.read_int}) — the document shape " \
|
|
137
|
+
"disagrees with the plan"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
PlanResult.read(raw, src, @names)
|
|
142
|
+
ensure
|
|
143
|
+
::Yeptris::FFI.yeptris_plan_result_free(raw)
|
|
144
|
+
end
|
|
145
|
+
ensure
|
|
146
|
+
::Yeptris::FFI.yeptris_tape_free(tape)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def column_count
|
|
151
|
+
::Yeptris::FFI.yeptris_plan_column_count(@handle)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The eager columnar result of Descriptor#walk: typed Ruby columns
|
|
155
|
+
# keyed by leaf name, plus the row-hash conveniences. Nothing here
|
|
156
|
+
# borrows C memory.
|
|
157
|
+
class PlanResult
|
|
158
|
+
# @api private — one bulk read per column; the C result is freed
|
|
159
|
+
# on return (PlanResult owns plain Ruby data)
|
|
160
|
+
def self.read(raw, src, names)
|
|
161
|
+
rows = ::Yeptris::FFI.yeptris_plan_result_rows(raw)
|
|
162
|
+
columns = {}
|
|
163
|
+
names.each_with_index do |name, c|
|
|
164
|
+
kind = ::Yeptris::FFI.yeptris_plan_result_kind(raw, c)
|
|
165
|
+
columns[name] = read_column(raw, c, kind, rows, src)
|
|
166
|
+
end
|
|
167
|
+
new(rows, names, columns)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.read_column(raw, c, kind, rows, src)
|
|
171
|
+
return [] if rows.zero?
|
|
172
|
+
|
|
173
|
+
nulls = ::Yeptris::FFI.yeptris_plan_result_nulls(raw, c).read_bytes(rows).unpack("C*")
|
|
174
|
+
has_nulls = nulls.include?(1)
|
|
175
|
+
case kind
|
|
176
|
+
when 0 # int: the unpacked lane IS the column when no nulls
|
|
177
|
+
ints = ::Yeptris::FFI.yeptris_plan_result_ints(raw, c).read_bytes(rows * 8).unpack("q<*")
|
|
178
|
+
return ints unless has_nulls
|
|
179
|
+
|
|
180
|
+
Array.new(rows) { |i| nulls[i] == 1 ? nil : ints[i] }
|
|
181
|
+
when 1
|
|
182
|
+
floats = ::Yeptris::FFI.yeptris_plan_result_floats(raw, c).read_bytes(rows * 8).unpack("E*")
|
|
183
|
+
return floats unless has_nulls
|
|
184
|
+
|
|
185
|
+
Array.new(rows) { |i| nulls[i] == 1 ? nil : floats[i] }
|
|
186
|
+
when 3 # bool: 0/1 → false/true (always maps)
|
|
187
|
+
ints = ::Yeptris::FFI.yeptris_plan_result_ints(raw, c).read_bytes(rows * 8).unpack("q<*")
|
|
188
|
+
Array.new(rows) { |i| nulls[i] == 1 ? nil : ints[i] == 1 }
|
|
189
|
+
else # str: spans into the source, escapes decoded on the Ruby side
|
|
190
|
+
offs = ::Yeptris::FFI.yeptris_plan_result_str_offs(raw, c).read_bytes(rows * 4).unpack("V*")
|
|
191
|
+
lens = ::Yeptris::FFI.yeptris_plan_result_str_lens(raw, c).read_bytes(rows * 4).unpack("V*")
|
|
192
|
+
decode = ::Yeptris::JSON.method(:decode_span)
|
|
193
|
+
Array.new(rows) { |i| nulls[i] == 1 ? nil : decode.call(src, offs[i], lens[i]) }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
private_class_method :read_column
|
|
197
|
+
|
|
198
|
+
# @api private — the DOM leg's read: typed lanes as above; str
|
|
199
|
+
# columns materialize from the (ptr,len) views into the document
|
|
200
|
+
def self.read_dom(raw, names)
|
|
201
|
+
rows = ::Yeptris::FFI.yeptris_plan_result_rows(raw)
|
|
202
|
+
columns = {}
|
|
203
|
+
names.each_with_index do |name, c|
|
|
204
|
+
kind = ::Yeptris::FFI.yeptris_plan_result_kind(raw, c)
|
|
205
|
+
if kind == 2
|
|
206
|
+
nulls = ::Yeptris::FFI.yeptris_plan_result_nulls(raw, c).read_bytes(rows).unpack("C*")
|
|
207
|
+
base = ::FFI::Pointer.new(::Yeptris::FFI.yeptris_plan_result_strs(raw, c))
|
|
208
|
+
stride = ::Yeptris::FFI::PlanStr.size
|
|
209
|
+
columns[name] = Array.new(rows) do |i|
|
|
210
|
+
next nil if nulls[i] == 1
|
|
211
|
+
|
|
212
|
+
v = ::Yeptris::FFI::PlanStr.new(base + i * stride)
|
|
213
|
+
v[:p].read_bytes(v[:len]).force_encoding(Encoding::UTF_8)
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
columns[name] = read_column(raw, c, kind, rows, nil)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
new(rows, names, columns)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def initialize(rows, names, columns)
|
|
223
|
+
@rows = rows
|
|
224
|
+
@names = names
|
|
225
|
+
@columns = columns
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def rows
|
|
229
|
+
@rows
|
|
230
|
+
end
|
|
231
|
+
alias count rows
|
|
232
|
+
|
|
233
|
+
# The planned leaf names, in spec order.
|
|
234
|
+
def names
|
|
235
|
+
@names.dup
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# One typed column: an Array of rows values (nil where the leaf
|
|
239
|
+
# was null, missing, or shape-mismatched).
|
|
240
|
+
def column(name)
|
|
241
|
+
@columns.fetch(name) { raise ArgumentError, "no planned column #{name.inspect}" }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Row +i+ as a Hash of the planned leaves (nil out of range).
|
|
245
|
+
def at(i)
|
|
246
|
+
return nil if i >= @rows || i.negative?
|
|
247
|
+
|
|
248
|
+
row = {}
|
|
249
|
+
@names.each { |n| row[n] = @columns[n][i] }
|
|
250
|
+
row
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Rows as Hashes — the tree-shaped convenience; column readers
|
|
254
|
+
# are the allocation-lean path.
|
|
255
|
+
def to_a
|
|
256
|
+
Array.new(@rows) { |i| at(i) }
|
|
257
|
+
end
|
|
258
|
+
alias to_ruby to_a # leptris's eager-tree vocabulary
|
|
259
|
+
|
|
260
|
+
def each_row(&block)
|
|
261
|
+
return enum_for(:each_row) unless block
|
|
262
|
+
|
|
263
|
+
@rows.times { |i| block.call(at(i)) }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
data/lib/yeptris/json.rb
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yeptris
|
|
4
|
+
# The STRICT JSON surface (TODO.restructure/31).
|
|
5
|
+
#
|
|
6
|
+
# `Yeptris::JSON.load` targets EXACT `JSON.parse` semantics — that
|
|
7
|
+
# is its parity target, spec-pinned (spec/json_parity_spec.rb).
|
|
8
|
+
# It is deliberately separate from Yeptris::YAML: the YAML surface
|
|
9
|
+
# keeps the Psych contract for every input (JSON-shaped included),
|
|
10
|
+
# so the two never drift into each other's semantics.
|
|
11
|
+
#
|
|
12
|
+
# Engines, fastest first (both exact — the parity spec runs against
|
|
13
|
+
# whichever is loaded):
|
|
14
|
+
# 1. the native materializer (fused C scan → VALUE; opt-in build)
|
|
15
|
+
# 2. the record drain + strict conversion walk (always available)
|
|
16
|
+
module JSON
|
|
17
|
+
class Error < ::Yeptris::Error; end
|
|
18
|
+
class ParseError < Error; end
|
|
19
|
+
|
|
20
|
+
autoload :Descriptor, "yeptris/json/descriptor"
|
|
21
|
+
|
|
22
|
+
# json gem 3.0 made duplicate keys an error by DEFAULT; 2.x is
|
|
23
|
+
# last-wins. The parity target is the RESOLVED json gem's own
|
|
24
|
+
# behavior — strictness follows it (issue #37, found by canon's
|
|
25
|
+
# CI where json 3.0.0 resolved while the dev box had 2.x).
|
|
26
|
+
require "json"
|
|
27
|
+
require "date" # place_obj's `when Date` — psych loads it elsewhere, JSON.dump must not depend on that
|
|
28
|
+
STRICT_DUPLICATE_KEYS = Gem::Version.new(::JSON::VERSION) >= Gem::Version.new("3")
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def load(source)
|
|
33
|
+
source = ::Yeptris.read_input(source)
|
|
34
|
+
source = source.to_s
|
|
35
|
+
if defined?(::Yeptris::Native)
|
|
36
|
+
begin
|
|
37
|
+
return ::Yeptris::Native.load_json(source, STRICT_DUPLICATE_KEYS)
|
|
38
|
+
rescue ::Yeptris::ParseError => e
|
|
39
|
+
raise ParseError, e.message
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
tape_engine(source)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The tape engine (issue #81): ONE call validates and materializes
|
|
46
|
+
# the record stream; the walk reads four bulk columns and slices
|
|
47
|
+
# strings straight out of the caller's source — no gate document,
|
|
48
|
+
# no recorder transform, no arena copy (the drain path paid all
|
|
49
|
+
# three, which is where medium/large throughput went).
|
|
50
|
+
T_DOC = 0
|
|
51
|
+
T_NULL = 1
|
|
52
|
+
T_TRUE = 2
|
|
53
|
+
T_FALSE = 3
|
|
54
|
+
T_INT = 4
|
|
55
|
+
T_FLOAT = 5
|
|
56
|
+
T_STR = 6
|
|
57
|
+
T_SEQ_OPEN = 7
|
|
58
|
+
T_MAP_OPEN = 8
|
|
59
|
+
T_CLOSE = 9
|
|
60
|
+
|
|
61
|
+
# The tape keeps RAW string spans (escapes untouched — the C
|
|
62
|
+
# contract); the walk decodes them here. The hot path (no
|
|
63
|
+
# backslash) returns the slice untouched.
|
|
64
|
+
SIMPLE_ESCAPES = {
|
|
65
|
+
'"' => '"'.b, "\\" => "\\".b, "/" => "/".b, "b" => "\b".b, "f" => "\f".b,
|
|
66
|
+
"n" => "\n".b, "r" => "\r".b, "t" => "\t".b
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
def decode_span(src, off, len)
|
|
70
|
+
s = src.byteslice(off, len)
|
|
71
|
+
s.force_encoding(Encoding::UTF_8)
|
|
72
|
+
return s unless s.include?("\\")
|
|
73
|
+
|
|
74
|
+
b = s.b
|
|
75
|
+
out = String.new(encoding: Encoding::BINARY)
|
|
76
|
+
i = 0
|
|
77
|
+
n = b.bytesize
|
|
78
|
+
while i < n
|
|
79
|
+
c = b.getbyte(i)
|
|
80
|
+
if c != 0x5C || i + 1 >= n
|
|
81
|
+
out << c
|
|
82
|
+
i += 1
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
e = b.getbyte(i + 1)
|
|
86
|
+
if e != 0x75 # simple escape
|
|
87
|
+
ch = e.chr
|
|
88
|
+
out << (SIMPLE_ESCAPES[ch] || ch)
|
|
89
|
+
i += 2
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
cp = b.byteslice(i + 2, 4).to_i(16)
|
|
93
|
+
if cp >= 0xD800 && cp <= 0xDBFF && i + 12 <= n &&
|
|
94
|
+
b.getbyte(i + 6) == 0x5C && b.getbyte(i + 7) == 0x75
|
|
95
|
+
lo = b.byteslice(i + 8, 4).to_i(16)
|
|
96
|
+
if lo >= 0xDC00 && lo <= 0xDFFF
|
|
97
|
+
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00)
|
|
98
|
+
i += 12
|
|
99
|
+
else
|
|
100
|
+
i += 6
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
i += 6
|
|
104
|
+
end
|
|
105
|
+
out << [cp].pack("U").b
|
|
106
|
+
end
|
|
107
|
+
out.force_encoding(Encoding::UTF_8)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def tape_engine(source)
|
|
111
|
+
tape = ::Yeptris::FFI::JsonTape.new
|
|
112
|
+
rc = ::Yeptris::FFI.yeptris_parse_json_tape(source, source.bytesize, tape)
|
|
113
|
+
raise ParseError, ::Yeptris::FFI.last_error_message if rc != ::Yeptris::FFI::OK
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
walk_tape(source, tape)
|
|
117
|
+
ensure
|
|
118
|
+
::Yeptris::FFI.yeptris_tape_free(tape)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def walk_tape(src, tape, strict_dup = STRICT_DUPLICATE_KEYS)
|
|
123
|
+
n = tape[:count]
|
|
124
|
+
kinds = tape[:kinds].read_bytes(n).unpack("C*")
|
|
125
|
+
offs = tape[:offs].read_bytes(n * 4).unpack("V*")
|
|
126
|
+
lens = tape[:lens].read_bytes(n * 4).unpack("V*")
|
|
127
|
+
# v2: parse records number spans only — ONE bulk convert call
|
|
128
|
+
# materializes them (per-number FFI calls would sink the ladder)
|
|
129
|
+
ivp = ::FFI::MemoryPointer.new(:int64, n, true)
|
|
130
|
+
dvp = ::FFI::MemoryPointer.new(:double, n, true)
|
|
131
|
+
::Yeptris::FFI.yeptris_tape_convert(tape, 0, n, ivp, dvp)
|
|
132
|
+
vals_i = ivp.read_bytes(n * 8).unpack("q<*")
|
|
133
|
+
vals_f = dvp.read_bytes(n * 8).unpack("E*")
|
|
134
|
+
# any integer text beyond int64: every INT rebuilds from its span
|
|
135
|
+
# (exact Bignum; JSON.parse parity) — int_min is set by the
|
|
136
|
+
# convert call above
|
|
137
|
+
exact_ints = !tape[:int_min].zero?
|
|
138
|
+
utf8 = src.encoding == Encoding::UTF_8
|
|
139
|
+
|
|
140
|
+
docs = [nil] # record 0 (DOC) pre-consumed — the root's slot
|
|
141
|
+
stack = []
|
|
142
|
+
key_sets = strict_dup ? [{}] : nil
|
|
143
|
+
pending_key = nil
|
|
144
|
+
i = 1
|
|
145
|
+
while i < n
|
|
146
|
+
case kinds[i]
|
|
147
|
+
when T_SEQ_OPEN
|
|
148
|
+
place(docs, stack, pending_key) { [] }
|
|
149
|
+
pending_key = nil
|
|
150
|
+
when T_MAP_OPEN
|
|
151
|
+
key_sets&.push({})
|
|
152
|
+
place(docs, stack, pending_key) { {} }
|
|
153
|
+
pending_key = nil
|
|
154
|
+
when T_CLOSE
|
|
155
|
+
# Key frames are pushed per MAP_OPEN — an ARRAY close must
|
|
156
|
+
# not pop the enclosing map's frame (issue found by ea's CI:
|
|
157
|
+
# {"a":[1],"b":[2],"c":[3]} exhausted the frames and the next
|
|
158
|
+
# map key hit key_sets.last.key? on nil under strict mode).
|
|
159
|
+
key_sets&.pop if stack.last.is_a?(Hash)
|
|
160
|
+
stack.pop
|
|
161
|
+
when T_STR
|
|
162
|
+
# empty-string literals are frozen+shared since Ruby 3.4
|
|
163
|
+
# (and `+""` binds after the method chain anyway), so the
|
|
164
|
+
# fresh copy must come from the encoding call itself
|
|
165
|
+
text = lens[i] == 0 ? String.new(encoding: Encoding::UTF_8) : decode_span(src, offs[i], lens[i])
|
|
166
|
+
if pending_key.nil? && !stack.empty? && stack.last.is_a?(Hash)
|
|
167
|
+
if key_sets && key_sets.last.key?(text)
|
|
168
|
+
raise ParseError, %(duplicate key "#{text}" in JSON object)
|
|
169
|
+
end
|
|
170
|
+
key_sets&.last&.store(text, true)
|
|
171
|
+
pending_key = text
|
|
172
|
+
else
|
|
173
|
+
place(docs, stack, pending_key) { text }
|
|
174
|
+
pending_key = nil
|
|
175
|
+
end
|
|
176
|
+
when T_INT
|
|
177
|
+
if exact_ints
|
|
178
|
+
place(docs, stack, pending_key) { Integer(src.byteslice(offs[i], lens[i]), 10) }
|
|
179
|
+
else
|
|
180
|
+
v = vals_i[i]
|
|
181
|
+
place(docs, stack, pending_key) { v }
|
|
182
|
+
end
|
|
183
|
+
pending_key = nil
|
|
184
|
+
when T_FLOAT
|
|
185
|
+
v = vals_f[i]
|
|
186
|
+
place(docs, stack, pending_key) { v }
|
|
187
|
+
pending_key = nil
|
|
188
|
+
when T_TRUE, T_FALSE # v2: the kind IS the value
|
|
189
|
+
place(docs, stack, pending_key) { kinds[i] == T_TRUE }
|
|
190
|
+
pending_key = nil
|
|
191
|
+
when T_NULL
|
|
192
|
+
place(docs, stack, pending_key) { nil }
|
|
193
|
+
pending_key = nil
|
|
194
|
+
else
|
|
195
|
+
raise Error, "internal: impossible tape record #{kinds[i]}"
|
|
196
|
+
end
|
|
197
|
+
i += 1
|
|
198
|
+
end
|
|
199
|
+
docs.empty? ? nil : docs.first
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# The always-available engine: the strict-JSON validator gates
|
|
203
|
+
# (parse_json raises on anything RFC 8259 rejects), then the
|
|
204
|
+
# value records convert WITHOUT the Psych quirk table — floats
|
|
205
|
+
# are always Floats, bools always bools ("1e3" is 1000.0 here
|
|
206
|
+
# and a String on the YAML surface; each surface is its own
|
|
207
|
+
# contract).
|
|
208
|
+
def strict_fallback(source)
|
|
209
|
+
begin
|
|
210
|
+
gate = ::Yeptris::Document.parse_json(source)
|
|
211
|
+
rescue ::Yeptris::ParseError => e
|
|
212
|
+
raise ParseError, e.message
|
|
213
|
+
end
|
|
214
|
+
begin
|
|
215
|
+
cols = ::Yeptris::FFI::ValueColumns.new
|
|
216
|
+
st = ::Yeptris::FFI.yeptris_value_drain_columns(
|
|
217
|
+
source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
|
|
218
|
+
)
|
|
219
|
+
raise ParseError, ::Yeptris::FFI.last_error_message if st != ::Yeptris::FFI::OK
|
|
220
|
+
|
|
221
|
+
begin
|
|
222
|
+
walk_strict(cols, STRICT_DUPLICATE_KEYS)
|
|
223
|
+
ensure
|
|
224
|
+
::Yeptris::FFI.yeptris_value_free_columns(cols)
|
|
225
|
+
end
|
|
226
|
+
ensure
|
|
227
|
+
gate.free
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Placement mechanics mirror ValueML.walk_columns; the CONVERSION
|
|
232
|
+
# is the strict-JSON one (no ':sym' scan, no y/n quirk, no
|
|
233
|
+
# dot-required floats). Anchors/aliases/timestamps cannot occur
|
|
234
|
+
# in strict JSON — reaching them is an internal error.
|
|
235
|
+
def walk_strict(cols, strict_dup = STRICT_DUPLICATE_KEYS)
|
|
236
|
+
n = cols[:count]
|
|
237
|
+
kinds = cols[:kinds].read_bytes(n).unpack("C*")
|
|
238
|
+
ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
|
|
239
|
+
bools = cols[:bools].read_bytes(n).unpack("C*")
|
|
240
|
+
offs = cols[:offs].read_bytes(n * 4).unpack("V*")
|
|
241
|
+
lens = cols[:lens].read_bytes(n * 4).unpack("V*")
|
|
242
|
+
pays = cols[:payloads].read_bytes(n * 8).unpack("q<*")
|
|
243
|
+
arena = cols[:arena_len].zero? ? +"" : cols[:arena].read_bytes(cols[:arena_len])
|
|
244
|
+
arena.force_encoding(Encoding::UTF_8)
|
|
245
|
+
|
|
246
|
+
docs = []
|
|
247
|
+
stack = []
|
|
248
|
+
key_sets = strict_dup ? [{}] : nil
|
|
249
|
+
pending_key = nil
|
|
250
|
+
i = 0
|
|
251
|
+
while i < n
|
|
252
|
+
case kinds[i]
|
|
253
|
+
when ValueML::DOC
|
|
254
|
+
docs.push(nil)
|
|
255
|
+
when ValueML::SEQ_OPEN
|
|
256
|
+
place(docs, stack, pending_key) { [] }
|
|
257
|
+
pending_key = nil
|
|
258
|
+
when ValueML::MAP_OPEN
|
|
259
|
+
key_sets&.push({})
|
|
260
|
+
place(docs, stack, pending_key) { {} }
|
|
261
|
+
pending_key = nil
|
|
262
|
+
when ValueML::CLOSE
|
|
263
|
+
key_sets&.pop if stack.last.is_a?(Hash)
|
|
264
|
+
stack.pop
|
|
265
|
+
when ValueML::V_STR
|
|
266
|
+
text = arena.byteslice(offs[i], lens[i])
|
|
267
|
+
if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
|
|
268
|
+
if strict_dup && key_sets.last.key?(text)
|
|
269
|
+
raise ParseError, %(duplicate key "#{text}" in JSON object)
|
|
270
|
+
end
|
|
271
|
+
key_sets&.last&.store(text, true)
|
|
272
|
+
pending_key = text
|
|
273
|
+
else
|
|
274
|
+
# Records carry int64 payloads: an integer-beyond-int64
|
|
275
|
+
# degrades to a PLAIN string (b==1). In strict JSON a
|
|
276
|
+
# plain (unquoted) scalar can ONLY be a number — every
|
|
277
|
+
# real string is quoted and arrives b==0 — so rebuild the
|
|
278
|
+
# exact Integer (JSON.parse parity, Bignum included).
|
|
279
|
+
if bools[i] == 1
|
|
280
|
+
place(docs, stack, pending_key) { Integer(text, 10) }
|
|
281
|
+
else
|
|
282
|
+
place(docs, stack, pending_key) { text }
|
|
283
|
+
end
|
|
284
|
+
pending_key = nil
|
|
285
|
+
end
|
|
286
|
+
when ValueML::V_INT
|
|
287
|
+
place(docs, stack, pending_key) { pays[i] }
|
|
288
|
+
pending_key = nil
|
|
289
|
+
when ValueML::V_FLOAT
|
|
290
|
+
place(docs, stack, pending_key) { [pays[i]].pack("q<").unpack1("E") }
|
|
291
|
+
pending_key = nil
|
|
292
|
+
when ValueML::V_BOOL
|
|
293
|
+
place(docs, stack, pending_key) { bools[i] == 1 }
|
|
294
|
+
pending_key = nil
|
|
295
|
+
when ValueML::V_NULL
|
|
296
|
+
place(docs, stack, pending_key) { nil }
|
|
297
|
+
pending_key = nil
|
|
298
|
+
else
|
|
299
|
+
raise Error, "internal: impossible record #{kinds[i]} in strict JSON"
|
|
300
|
+
end
|
|
301
|
+
i += 1
|
|
302
|
+
end
|
|
303
|
+
docs.empty? ? nil : docs.first
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# ---- generation (issue #81) ------------------------------------------
|
|
307
|
+
#
|
|
308
|
+
# `dump` targets JSON.generate semantics for the core types: the
|
|
309
|
+
# object walk reuses YAML.dump's packed-entry bulk builder (one
|
|
310
|
+
# FFI build call), then the strict-JSON writer serializes —
|
|
311
|
+
# Strings ride DOUBLE_QUOTED (a "42" stays quoted; numbers, bools
|
|
312
|
+
# and null stay plain), Symbols become their name.
|
|
313
|
+
class DumpError < Error; end
|
|
314
|
+
|
|
315
|
+
def dump(obj)
|
|
316
|
+
parts = String.new(encoding: Encoding::BINARY,
|
|
317
|
+
capacity: 12 * 16) # 12-byte entries, appended in place
|
|
318
|
+
blob = String.new(encoding: Encoding::BINARY)
|
|
319
|
+
off = [0]
|
|
320
|
+
place_obj(obj, parts, blob, off)
|
|
321
|
+
doc = ::Yeptris::Document.create
|
|
322
|
+
# FFI passes String args BY REFERENCE — no MemoryPointer copies
|
|
323
|
+
rc = doc.build_entries(parts, parts.bytesize / 12, blob, blob.bytesize)
|
|
324
|
+
raise DumpError, "document_build failed: #{rc}" unless rc == ::Yeptris::FFI::OK
|
|
325
|
+
out = ::Yeptris::FFI::JSON_EX ? doc.serialize_json_compact : doc.serialize_json
|
|
326
|
+
# JSON.generate emits no trailing newline (the document writer does)
|
|
327
|
+
out.end_with?("\n") ? out[0, out.bytesize - 1] : out
|
|
328
|
+
ensure
|
|
329
|
+
doc&.free
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
F = ::Yeptris::FFI
|
|
333
|
+
MAP_E = [F::BUILD_MAP, 0, 0, 0].pack("CCx2VV").freeze
|
|
334
|
+
SEQ_E = [F::BUILD_SEQ, 0, 0, 0].pack("CCx2VV").freeze
|
|
335
|
+
END_E = [F::BUILD_END, 0, 0, 0].pack("CCx2VV").freeze
|
|
336
|
+
DQ = F::STYLE_DOUBLE_QUOTED
|
|
337
|
+
PL = F::STYLE_PLAIN
|
|
338
|
+
SC_DQ = [F::BUILD_SCALAR, DQ].pack("CCx2").freeze
|
|
339
|
+
SC_PL = [F::BUILD_SCALAR, PL].pack("CCx2").freeze
|
|
340
|
+
|
|
341
|
+
def place_obj(obj, parts, blob, off)
|
|
342
|
+
case obj
|
|
343
|
+
when Hash
|
|
344
|
+
parts << MAP_E
|
|
345
|
+
obj.each do |k, v|
|
|
346
|
+
key = k.is_a?(String) ? k : k.is_a?(Symbol) ? k.name : k.to_s
|
|
347
|
+
scalar_json(key, SC_DQ, parts, blob, off)
|
|
348
|
+
place_obj(v, parts, blob, off)
|
|
349
|
+
end
|
|
350
|
+
parts << END_E
|
|
351
|
+
when Array
|
|
352
|
+
parts << SEQ_E
|
|
353
|
+
obj.each { |e| place_obj(e, parts, blob, off) }
|
|
354
|
+
parts << END_E
|
|
355
|
+
when String
|
|
356
|
+
scalar_json(obj, SC_DQ, parts, blob, off)
|
|
357
|
+
when Symbol
|
|
358
|
+
scalar_json(obj.name, SC_DQ, parts, blob, off)
|
|
359
|
+
when Integer, Float
|
|
360
|
+
scalar_json(obj.to_s, SC_PL, parts, blob, off)
|
|
361
|
+
when true
|
|
362
|
+
scalar_json("true", SC_PL, parts, blob, off)
|
|
363
|
+
when false
|
|
364
|
+
scalar_json("false", SC_PL, parts, blob, off)
|
|
365
|
+
when nil
|
|
366
|
+
scalar_json("null", SC_PL, parts, blob, off)
|
|
367
|
+
when Date, Time
|
|
368
|
+
scalar_json(obj.iso8601, SC_DQ, parts, blob, off)
|
|
369
|
+
else
|
|
370
|
+
raise DumpError, "cannot dump #{obj.class}: unsupported object " \
|
|
371
|
+
"(JSON.generate calls to_json on custom types)"
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def scalar_json(text, prefix, parts, blob, off)
|
|
376
|
+
parts << (prefix + [off[0], text.bytesize].pack("VV"))
|
|
377
|
+
blob << text
|
|
378
|
+
off[0] += text.bytesize
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def place(docs, stack, key)
|
|
382
|
+
v = yield
|
|
383
|
+
if stack.empty?
|
|
384
|
+
docs[-1] = v
|
|
385
|
+
elsif key
|
|
386
|
+
stack.last[key] = v
|
|
387
|
+
else
|
|
388
|
+
stack.last.push(v)
|
|
389
|
+
end
|
|
390
|
+
stack.push(v) if v.is_a?(Array) || v.is_a?(Hash)
|
|
391
|
+
v
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|