yeptris 0.6.3.2 → 0.6.4.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: 71f8626c8109cc1d84ee3593a0646a4005b22e00fad770426024fcd783ddf123
4
- data.tar.gz: 34293da3ac105daa501ae1e7cc343294035341608696eb9f59bdea789e0cc4a8
3
+ metadata.gz: d45b2a3218406977adea5d7ae6d9184042864fce7bd2aeb9e69f3d87f0d11351
4
+ data.tar.gz: f2c61980fb7ca80b0d5686820718594c9cf8a0290975b555b2ce777d136bbfad
5
5
  SHA512:
6
- metadata.gz: c040550f2f2cfa3b30b1dfca891c306dd2ce390471457575be118098bc16718f48b6031d4c332b18f063556bbaad422135de220e924314291fae6c7438361461
7
- data.tar.gz: 8cfc1cf59607134a53ea27dae7a5af0de6801ac251de429863f45ca899566b631638c70e7c4d863e9eb997df562fd3c9a6e2ac7d386bb01193efcbffeee6213d
6
+ metadata.gz: ef6d04e084a9bf84458396a560176ccd56eacd375bbc8bbf215e6ba810f2e07d149cd95727842fd5864f7eafc90306a09395becc154f16e68e4ec43fcc183322
7
+ data.tar.gz: da3bc2434c554b1d656e4f2749a1ee98acaaef71d37f5fad4372da679e22ff1c15b5be55545f18cb4e62e3984fc409f8763f563bd1b21d682c765b5f4af8b59b
data/lib/yeptris/ffi.rb CHANGED
@@ -75,6 +75,33 @@ module Yeptris
75
75
  attach_function :yeptris_tape_free, [:pointer], :void
76
76
  attach_function :yeptris_tape_convert, %i[pointer size_t size_t pointer pointer], :size_t
77
77
 
78
+ # the compiled plan walk (#293 / TODO.restructure/87): compile a
79
+ # strict-JSON spec once, apply it to a parsed tape in one C pass,
80
+ # read the typed COLUMNAR result
81
+ attach_function :yeptris_plan_compile, %i[pointer size_t pointer], :pointer
82
+ attach_function :yeptris_plan_free, [:pointer], :void
83
+ attach_function :yeptris_plan_column_count, [:pointer], :size_t
84
+ attach_function :yeptris_tape_plan_walk, %i[pointer pointer pointer], :pointer
85
+ attach_function :yeptris_plan_result_free, [:pointer], :void
86
+ attach_function :yeptris_plan_result_rows, [:pointer], :size_t
87
+ attach_function :yeptris_plan_result_kind, %i[pointer size_t], :int
88
+ attach_function :yeptris_plan_result_ints, %i[pointer size_t], :pointer
89
+ attach_function :yeptris_plan_result_floats, %i[pointer size_t], :pointer
90
+ attach_function :yeptris_plan_result_str_offs, %i[pointer size_t], :pointer
91
+ attach_function :yeptris_plan_result_str_lens, %i[pointer size_t], :pointer
92
+ attach_function :yeptris_plan_result_nulls, %i[pointer size_t], :pointer
93
+
94
+ # the DOM (YAML) leg of the plan walk (#293 slice three): same
95
+ # compiled plan over a parsed document; str columns expose
96
+ # (ptr,len) views into the document's regions
97
+ attach_function :yeptris_document_plan_walk, %i[yeptris_document pointer pointer], :pointer
98
+ attach_function :yeptris_plan_result_strs, %i[pointer size_t], :pointer
99
+
100
+ # yeptris_plan_str (plan.h): one string view
101
+ class PlanStr < ::FFI::Struct
102
+ layout :p, :pointer, :len, :size_t
103
+ end
104
+
78
105
  attach_function :yeptris_document_free, [:yeptris_document], :void
79
106
  attach_function :yeptris_document_count, [:yeptris_document], :size_t
80
107
  attach_function :yeptris_document_root, [:yeptris_document, :size_t], :yeptris_node
@@ -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 CHANGED
@@ -17,6 +17,8 @@ module Yeptris
17
17
  class Error < ::Yeptris::Error; end
18
18
  class ParseError < Error; end
19
19
 
20
+ autoload :Descriptor, "yeptris/json/descriptor"
21
+
20
22
  # json gem 3.0 made duplicate keys an error by DEFAULT; 2.x is
21
23
  # last-wins. The parity target is the RESOLVED json gem's own
22
24
  # behavior — strictness follows it (issue #37, found by canon's
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The YAML leg of the Descriptor plan walk (yeptris#293 slice three,
4
+ # over the C DOM plan walk of libyeptris v0.6.4): the same compiled
5
+ # row shape as Yeptris::JSON::Descriptor, applied to a parsed YAML
6
+ # document — block YAML hydrates to typed columns with no
7
+ # intermediate Ruby tree.
8
+ #
9
+ # descriptor = Yeptris::YAML::Descriptor.build(
10
+ # kind: :map, path: ["data", "items"], # segmented paths work
11
+ # children: [{ name: "id", kind: :int }, { name: "name", kind: :str }])
12
+ # descriptor.walk(yaml).column("id") # => [1, 2, ...]
13
+ #
14
+ # Typed extraction rides the parse-time tag ids (schema: selects the
15
+ # resolver — :compat_11 keeps the Psych/libyaml implicit typing
16
+ # YAML.load uses); string columns are materialized from the
17
+ # document's regions before the result returns (nothing borrowed).
18
+ class Yeptris::YAML::Descriptor < ::Yeptris::JSON::Descriptor
19
+ # schema: :core_12 (YAML 1.2) or :compat_11 (the Psych-compatible
20
+ # implicit typing Yeptris::YAML.load uses).
21
+ def initialize(handle, names, schema)
22
+ super(handle, names)
23
+ @schema = schema
24
+ end
25
+
26
+ # @api private — the compile rides the JSON Descriptor's grammar
27
+ # (inherited private class method); schema selects the resolver
28
+ def self.build(kind:, path: nil, children:, schema: :compat_11)
29
+ handle, names = compile_handle(kind, path, children)
30
+ new(handle, names, schema)
31
+ end
32
+
33
+ def walk(yaml)
34
+ src = ::Yeptris.read_input(yaml).to_s
35
+ doc = ::Yeptris::Document.parse(src, schema: @schema)
36
+ begin
37
+ st = ::FFI::MemoryPointer.new(:int)
38
+ raw = ::Yeptris::FFI.yeptris_document_plan_walk(doc.c_ptr, @handle, st)
39
+ if raw.null?
40
+ raise ::Yeptris::JSON::Descriptor::Error,
41
+ "plan walk failed (status=#{st.read_int}) — the document shape " \
42
+ "disagrees with the plan"
43
+ end
44
+
45
+ begin
46
+ ::Yeptris::JSON::Descriptor::PlanResult.read_dom(raw, @names)
47
+ ensure
48
+ ::Yeptris::FFI.yeptris_plan_result_free(raw)
49
+ end
50
+ ensure
51
+ doc.free
52
+ end
53
+ end
54
+ end
data/lib/yeptris/yaml.rb CHANGED
@@ -9,6 +9,8 @@ module Yeptris
9
9
  # the recorder-driven Visitors in phase B; this is the yeptris-native
10
10
  # face users target first).
11
11
  module YAML
12
+ autoload :Descriptor, "yeptris/yaml/descriptor"
13
+
12
14
  module_function
13
15
 
14
16
  # Loads the FIRST document of a YAML stream as native Ruby objects.
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.3.2".freeze
6
+ VERSION = "0.6.4.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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3.2
4
+ version: 0.6.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -42,6 +42,7 @@ files:
42
42
  - lib/yeptris/document.rb
43
43
  - lib/yeptris/ffi.rb
44
44
  - lib/yeptris/json.rb
45
+ - lib/yeptris/json/descriptor.rb
45
46
  - lib/yeptris/materializer.rb
46
47
  - lib/yeptris/node.rb
47
48
  - lib/yeptris/psych.rb
@@ -54,6 +55,7 @@ files:
54
55
  - lib/yeptris/schema.rb
55
56
  - lib/yeptris/valueml.rb
56
57
  - lib/yeptris/yaml.rb
58
+ - lib/yeptris/yaml/descriptor.rb
57
59
  homepage: https://github.com/leptris/yeptris
58
60
  licenses:
59
61
  - MIT