yeptris 0.6.3.2 → 0.6.3.3

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: 2d16340cd0d9b49195e34d5cad813470ef6d71a7b39ac902b0a1580999708cfc
4
+ data.tar.gz: 20f49034bd75ba72a1202c1b0bee77626aba0246719647ba3848d5849c54cd1b
5
5
  SHA512:
6
- metadata.gz: c040550f2f2cfa3b30b1dfca891c306dd2ce390471457575be118098bc16718f48b6031d4c332b18f063556bbaad422135de220e924314291fae6c7438361461
7
- data.tar.gz: 8cfc1cf59607134a53ea27dae7a5af0de6801ac251de429863f45ca899566b631638c70e7c4d863e9eb997df562fd3c9a6e2ac7d386bb01193efcbffeee6213d
6
+ metadata.gz: 4d77c4cfcb593eef407c9472b688f2ea527d45f4928a9a74a7dbbb90af16bdc681faf03db34276cf4f28ee963d0830af2796a365e94e229a506125a75db8301e
7
+ data.tar.gz: 37ff59642e868d3e9313067f424c0bfdd14f1b359052ff8c299636c14fd1c1453772f9df825134d5c07a60bf90581e2d3396b1fb9587e6658f046a65b68b9edb
data/lib/yeptris/ffi.rb CHANGED
@@ -75,6 +75,22 @@ 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
+
78
94
  attach_function :yeptris_document_free, [:yeptris_document], :void
79
95
  attach_function :yeptris_document_count, [:yeptris_document], :size_t
80
96
  attach_function :yeptris_document_root, [:yeptris_document, :size_t], :yeptris_node
@@ -0,0 +1,220 @@
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). +children+: the leaf columns; each row is a
54
+ # mapping, every child names one typed column.
55
+ def self.build(kind:, path: nil, children:)
56
+ unless ROOT_KINDS.include?(kind)
57
+ raise ArgumentError, "kind must be one of #{ROOT_KINDS.inspect}, got #{kind.inspect}"
58
+ end
59
+ unless children.is_a?(::Array) && !children.empty?
60
+ raise ArgumentError, "children must be a non-empty Array"
61
+ end
62
+
63
+ leaves = children.map do |child|
64
+ name = child[:name]
65
+ code = LEAF_KINDS[child[:kind]]
66
+ if code.nil?
67
+ raise ArgumentError,
68
+ "leaf kind must be one of #{LEAF_KINDS.keys.inspect}, got #{child[:kind].inspect}"
69
+ end
70
+ unless name.is_a?(::String) && !name.empty?
71
+ raise ArgumentError, "leaf name must be a non-empty String, got #{name.inspect}"
72
+ end
73
+
74
+ { "name" => name, "kind" => LEAF_SPEC_NAMES[code] }
75
+ end
76
+
77
+ spec = { "kind" => kind == :map ? "map" : "seq", "children" => leaves }
78
+ spec["path"] = path if path
79
+
80
+ # the spec is strict JSON (the C compiler parses it through the
81
+ # strict JSON DOM); compile once, the engine owns the copy
82
+ spec_json = ::JSON.generate(spec)
83
+ st = ::FFI::MemoryPointer.new(:int)
84
+ raw = ::Yeptris::FFI.yeptris_plan_compile(spec_json, spec_json.bytesize, st)
85
+ if raw.null?
86
+ raise Error, "plan compile failed (status=#{st.read_int}): #{spec_json}"
87
+ end
88
+
89
+ new(Handle.new(raw), leaves.map { |leaf| leaf["name"] })
90
+ end
91
+
92
+ attr_reader :names # the planned leaf names, in spec order
93
+
94
+ # @api private — handles come from .build
95
+ def initialize(handle, names)
96
+ @handle = handle
97
+ @names = names.dup.freeze
98
+ end
99
+
100
+ # Applies the plan to +json+ (String or IO): parse the tape, one C
101
+ # plan pass, bulk-read the typed columns. The returned PlanResult
102
+ # is standalone Ruby (nothing borrowed from the document).
103
+ def walk(json)
104
+ src = ::Yeptris.read_input(json).to_s
105
+ tape = ::Yeptris::FFI::JsonTape.new
106
+ rc = ::Yeptris::FFI.yeptris_parse_json_tape(src, src.bytesize, tape)
107
+ raise ::Yeptris::JSON::ParseError, ::Yeptris::FFI.last_error_message if rc != ::Yeptris::FFI::OK
108
+
109
+ begin
110
+ st = ::FFI::MemoryPointer.new(:int)
111
+ raw = ::Yeptris::FFI.yeptris_tape_plan_walk(tape, @handle, st)
112
+ if raw.null?
113
+ raise Error,
114
+ "plan walk failed (status=#{st.read_int}) — the document shape " \
115
+ "disagrees with the plan"
116
+ end
117
+
118
+ begin
119
+ PlanResult.read(raw, src, @names)
120
+ ensure
121
+ ::Yeptris::FFI.yeptris_plan_result_free(raw)
122
+ end
123
+ ensure
124
+ ::Yeptris::FFI.yeptris_tape_free(tape)
125
+ end
126
+ end
127
+
128
+ def column_count
129
+ ::Yeptris::FFI.yeptris_plan_column_count(@handle)
130
+ end
131
+
132
+ # The eager columnar result of Descriptor#walk: typed Ruby columns
133
+ # keyed by leaf name, plus the row-hash conveniences. Nothing here
134
+ # borrows C memory.
135
+ class PlanResult
136
+ # @api private — one bulk read per column; the C result is freed
137
+ # on return (PlanResult owns plain Ruby data)
138
+ def self.read(raw, src, names)
139
+ rows = ::Yeptris::FFI.yeptris_plan_result_rows(raw)
140
+ columns = {}
141
+ names.each_with_index do |name, c|
142
+ kind = ::Yeptris::FFI.yeptris_plan_result_kind(raw, c)
143
+ columns[name] = read_column(raw, c, kind, rows, src)
144
+ end
145
+ new(rows, names, columns)
146
+ end
147
+
148
+ def self.read_column(raw, c, kind, rows, src)
149
+ return [] if rows.zero?
150
+
151
+ nulls = ::Yeptris::FFI.yeptris_plan_result_nulls(raw, c).read_bytes(rows).unpack("C*")
152
+ has_nulls = nulls.include?(1)
153
+ case kind
154
+ when 0 # int: the unpacked lane IS the column when no nulls
155
+ ints = ::Yeptris::FFI.yeptris_plan_result_ints(raw, c).read_bytes(rows * 8).unpack("q<*")
156
+ return ints unless has_nulls
157
+
158
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : ints[i] }
159
+ when 1
160
+ floats = ::Yeptris::FFI.yeptris_plan_result_floats(raw, c).read_bytes(rows * 8).unpack("E*")
161
+ return floats unless has_nulls
162
+
163
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : floats[i] }
164
+ when 3 # bool: 0/1 → false/true (always maps)
165
+ ints = ::Yeptris::FFI.yeptris_plan_result_ints(raw, c).read_bytes(rows * 8).unpack("q<*")
166
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : ints[i] == 1 }
167
+ else # str: spans into the source, escapes decoded on the Ruby side
168
+ offs = ::Yeptris::FFI.yeptris_plan_result_str_offs(raw, c).read_bytes(rows * 4).unpack("V*")
169
+ lens = ::Yeptris::FFI.yeptris_plan_result_str_lens(raw, c).read_bytes(rows * 4).unpack("V*")
170
+ decode = ::Yeptris::JSON.method(:decode_span)
171
+ Array.new(rows) { |i| nulls[i] == 1 ? nil : decode.call(src, offs[i], lens[i]) }
172
+ end
173
+ end
174
+ private_class_method :read_column
175
+
176
+ def initialize(rows, names, columns)
177
+ @rows = rows
178
+ @names = names
179
+ @columns = columns
180
+ end
181
+
182
+ def rows
183
+ @rows
184
+ end
185
+ alias count rows
186
+
187
+ # The planned leaf names, in spec order.
188
+ def names
189
+ @names.dup
190
+ end
191
+
192
+ # One typed column: an Array of rows values (nil where the leaf
193
+ # was null, missing, or shape-mismatched).
194
+ def column(name)
195
+ @columns.fetch(name) { raise ArgumentError, "no planned column #{name.inspect}" }
196
+ end
197
+
198
+ # Row +i+ as a Hash of the planned leaves (nil out of range).
199
+ def at(i)
200
+ return nil if i >= @rows || i.negative?
201
+
202
+ row = {}
203
+ @names.each { |n| row[n] = @columns[n][i] }
204
+ row
205
+ end
206
+
207
+ # Rows as Hashes — the tree-shaped convenience; column readers
208
+ # are the allocation-lean path.
209
+ def to_a
210
+ Array.new(@rows) { |i| at(i) }
211
+ end
212
+ alias to_ruby to_a # leptris's eager-tree vocabulary
213
+
214
+ def each_row(&block)
215
+ return enum_for(:each_row) unless block
216
+
217
+ @rows.times { |i| block.call(at(i)) }
218
+ end
219
+ end
220
+ 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
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.3.3".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.3.3
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