yeptris 0.1.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.
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The neutral Ruby surface (the Psych-compat namespace arrives with
5
+ # the recorder-driven Visitors in phase B; this is the yeptris-native
6
+ # face users target first).
7
+ module YAML
8
+ module_function
9
+
10
+ # Loads the FIRST document of a YAML stream as native Ruby objects.
11
+ # schema: :compat_11 selects Psych/libyaml implicit typing
12
+ # (yes/no, 0o/octal, sexagesimal); :core_12 (default) is YAML 1.2.
13
+ def load(yaml, schema: :compat_11)
14
+ ValueML.load(yaml, schema: schema)
15
+ end
16
+
17
+ # Every document in the stream, in order.
18
+ def load_stream(yaml, schema: :compat_11)
19
+ ValueML.load_all(yaml, schema: schema)
20
+ end
21
+
22
+ # The Psych-suite port's spelling (spec/psych/): compat typing.
23
+ def parse_yaml(yaml)
24
+ load(yaml)
25
+ end
26
+
27
+ def load_file(path, schema: :compat_11)
28
+ File.open(path, "rb") { |f| load(f, schema: schema) }
29
+ end
30
+
31
+ # Parses without materializing: the first document's root Node.
32
+ def parse(yaml, schema: :core_12)
33
+ doc = Document.parse(yaml, schema: schema)
34
+ return nil if doc.document_count.zero?
35
+
36
+ doc.root(0)
37
+ end
38
+
39
+ # Serializes a Ruby object graph to YAML via the DOM builder
40
+ # (TODO.impl/11 phase 3). Strings are emitted plain only when the
41
+ # resolver round-trips them as strings — everything else takes a
42
+ # quoted style, so dump(load(x)) == x for the scalar types.
43
+ def dump(obj, canonical: false)
44
+ BulkBuilder.dump(obj, canonical: canonical)
45
+ end
46
+
47
+
48
+ # The dump-side mirror of the Materializer's bulk drain (TODO.impl
49
+ # 15 phase D): the tree walks into one flat entry array plus one
50
+ # string blob, and yeptris_document_build raises the DOM in a
51
+ # SINGLE FFI call — per-node FFI is gone. Same semantics as the
52
+ # per-node Builder it replaces (cycle refusal, :symbol scalars,
53
+ # Date/Time iso8601, plain-only-when-it-round-trips strings),
54
+ # with plain_string? pinned to the resolver by a differential
55
+ # spec.
56
+ module BulkBuilder
57
+ SCALAR = Yeptris::FFI::BUILD_SCALAR
58
+ SEQ = Yeptris::FFI::BUILD_SEQ
59
+ MAP = Yeptris::FFI::BUILD_MAP
60
+ STOP = Yeptris::FFI::BUILD_END
61
+ STYLE_PLAIN = 1
62
+ STYLE_DQ = 3
63
+
64
+ module_function
65
+
66
+ # container entries are constant bytes — no pack per op
67
+ MAP_ENTRY = [Yeptris::FFI::BUILD_MAP, 0, 0, 0].pack("CCx2VV").freeze
68
+ SEQ_ENTRY = [Yeptris::FFI::BUILD_SEQ, 0, 0, 0].pack("CCx2VV").freeze
69
+ END_ENTRY = [Yeptris::FFI::BUILD_END, 0, 0, 0].pack("CCx2VV").freeze
70
+ CONT_ENTRY = { Yeptris::FFI::BUILD_MAP => MAP_ENTRY,
71
+ Yeptris::FFI::BUILD_SEQ => SEQ_ENTRY,
72
+ Yeptris::FFI::BUILD_END => END_ENTRY }.freeze
73
+
74
+ def dump(obj, canonical: false)
75
+ parts = []
76
+ blob = +""
77
+ off = [0]
78
+ # one pack per SCALAR; containers reuse frozen constants
79
+ emit = lambda do |op, style, o, len|
80
+ parts << (o.zero? && len.zero? && style.zero? ? CONT_ENTRY[op] :
81
+ [op, style, o, len].pack("CCx2VV"))
82
+ end
83
+ place(obj, emit, blob, off, {})
84
+ doc = Document.create
85
+ buf = ::FFI::MemoryPointer.from_string(parts.join)
86
+ bblob = ::FFI::MemoryPointer.from_string(blob)
87
+ rc = doc.build_entries(buf, parts.length, bblob, blob.bytesize)
88
+ raise DumpError, "document_build failed: #{rc}" unless rc == FFI::OK
89
+ doc.serialize(canonical: canonical)
90
+ ensure
91
+ doc&.free
92
+ end
93
+
94
+ def place(obj, emit, blob, off, seen)
95
+ case obj
96
+ when Hash
97
+ cycle_guard(obj, seen) do
98
+ emit.call(MAP, 0, 0, 0)
99
+ obj.each do |k, v|
100
+ place(key_text(k), emit, blob, off, seen)
101
+ place(v, emit, blob, off, seen)
102
+ end
103
+ emit.call(STOP, 0, 0, 0)
104
+ end
105
+ when Array
106
+ cycle_guard(obj, seen) do
107
+ emit.call(SEQ, 0, 0, 0)
108
+ obj.each { |e| place(e, emit, blob, off, seen) }
109
+ emit.call(STOP, 0, 0, 0)
110
+ end
111
+ when String then scalar(obj, plain_string?(obj), emit, blob, off)
112
+ when Symbol then scalar(":#{obj}", true, emit, blob, off)
113
+ when Integer, Float then scalar(obj.to_s, true, emit, blob, off)
114
+ when true, false then scalar(obj.to_s, true, emit, blob, off)
115
+ when nil then scalar("null", true, emit, blob, off)
116
+ when Date, Time then scalar(obj.iso8601, true, emit, blob, off)
117
+ else
118
+ raise DumpError,
119
+ "cannot dump #{obj.class}: unsupported object " \
120
+ "(custom to_yaml support lands with the Psych Visitors)"
121
+ end
122
+ end
123
+
124
+ def scalar(text, plain, emit, blob, off)
125
+ bytes = text.b
126
+ emit.call(SCALAR, plain ? STYLE_PLAIN : STYLE_DQ, off[0], bytes.bytesize)
127
+ blob << bytes
128
+ off[0] += bytes.bytesize
129
+ end
130
+
131
+ def key_text(k)
132
+ k = ":#{k}" if k.is_a?(Symbol)
133
+ k.to_s
134
+ end
135
+
136
+ # A plain scalar that the compat resolver re-reads as STR stays
137
+ # plain; anything resolvable (null/bool words, int/float/timestamp
138
+ # shapes, indicators, merge '<<') takes double quotes so the
139
+ # reparse yields String again. Pinned to the C resolver's own
140
+ # verdicts by the differential spec (spec/yaml_spec.rb).
141
+ RESHAPES = %w[~ null Null NULL y Y yes Yes YES n N no No NO true True
142
+ TRUE false False FALSE on On ON off Off OFF <<].freeze
143
+
144
+ SAFE_WORD = /\A[A-Za-z][A-Za-z0-9_\-.\/ ]*\z/
145
+
146
+ def plain_string?(s)
147
+ # fast lane: letter-started, safe characters incl. spaces — no
148
+ # number/timestamp/indicator shape is possible; only the
149
+ # reshape words need the set lookup
150
+ if SAFE_WORD.match?(s) && !s.end_with?(" ") && !RESHAPES.include?(s)
151
+ return true
152
+ end
153
+ return false if s.empty? || s != s.strip
154
+ return false if s.match?(/[\n\t]/)
155
+ c = s[0]
156
+ return false if "#,[]{}&*!|>'\"%@`".include?(c)
157
+ return false if "-?:".include?(c) && (s.length == 1 || s[1] =~ /[ \t]/)
158
+ return false if s.include?(": ") || s.end_with?(":") || s.include?(" #")
159
+ return false if RESHAPES.include?(s)
160
+ # compat's float grammar REQUIRES the dot ("1e3" re-reads as a
161
+ # String and may dump plain); ints/sexagesimals still reshape
162
+ return false if s.match?(/\A[-+]?(0|[1-9][0-9_]*)(:[0-5]?[0-9])+\z/)
163
+ return false if s.match?(/\A[-+]?(0|[1-9][0-9_]*)\z/)
164
+ return false if s.match?(/\A[-+]?[0-9][0-9_]*\.[0-9_]*([eE][-+]?[0-9]+)?([.:][0-9_:.]*)?\z/)
165
+ return false if s.match?(/\A[-+]?(0x[0-9a-fA-F_]+|0b[01_]+|0o?[0-7_]+)\z/)
166
+ return false if s.match?(/\A[-+]?\.(inf|Inf|INF)\z|\A\.(nan|NaN|NAN)\z/)
167
+ !s.match?(/\A\d{4}-\d\d?-\d\d?([Tt ]|$)/)
168
+ end
169
+
170
+ def cycle_guard(obj, seen)
171
+ id = obj.object_id
172
+ raise DumpError, "cycle detected: cannot dump recursive #{obj.class}" if seen[id]
173
+
174
+ seen[id] = true
175
+ out = yield
176
+ seen.delete(id)
177
+ out
178
+ end
179
+ end
180
+
181
+ # From-scratch builder over the public construction API.
182
+ module Builder
183
+ module_function
184
+
185
+ def build(doc, obj, seen = {})
186
+ case obj
187
+ when Hash then build_map(doc, obj, seen)
188
+ when Array then build_seq(doc, obj, seen)
189
+ when String then build_string(doc, obj)
190
+ when Symbol then new_scalar(doc, ":#{obj}")
191
+ when Integer, Float then new_scalar(doc, obj.to_s)
192
+ when true, false then new_scalar(doc, obj.to_s)
193
+ when nil then new_scalar(doc, "null")
194
+ when Date, Time then new_scalar(doc, obj.iso8601)
195
+ else
196
+ raise DumpError,
197
+ "cannot dump #{obj.class}: unsupported object " \
198
+ "(custom to_yaml support lands with the Psych Visitors)"
199
+ end
200
+ end
201
+
202
+ def build_map(doc, h, seen)
203
+ cycle_guard(h, seen) do
204
+ m = doc.new_mapping
205
+ h.each { |k, v| m.map_add(key_text(k), build(doc, v, seen)) }
206
+ m
207
+ end
208
+ end
209
+
210
+ def build_seq(doc, a, seen)
211
+ cycle_guard(a, seen) do
212
+ s = doc.new_sequence
213
+ a.each { |e| s.seq_add(build(doc, e, seen)) }
214
+ s
215
+ end
216
+ end
217
+
218
+ # A plain scalar that re-resolves to STR stays plain (nice
219
+ # round-trips); anything ambiguous is double-quoted so the
220
+ # reparse yields String again.
221
+ def build_string(doc, s)
222
+ n = new_scalar(doc, s)
223
+ n.tag_id == :str ? n : new_scalar(doc, s, :force_str)
224
+ end
225
+
226
+ def key_text(k)
227
+ k = ":#{k}" if k.is_a?(Symbol)
228
+ k.to_s
229
+ end
230
+
231
+ def new_scalar(doc, text, mode = nil)
232
+ doc.new_scalar(text.to_s,
233
+ mode == :force_str ? :double_quoted : :plain)
234
+ end
235
+
236
+ def cycle_guard(obj, seen)
237
+ id = obj.object_id
238
+ raise DumpError, "cycle detected: cannot dump recursive #{obj.class}" if seen[id]
239
+
240
+ seen[id] = true
241
+ out = yield
242
+ seen.delete(id)
243
+ out
244
+ end
245
+ end
246
+ end
247
+ end
data/lib/yeptris.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yeptris/version"
4
+
5
+ module Yeptris
6
+ # eager: nested constants (Yeptris::ParseError etc.) do NOT trigger
7
+ # a parent-constant autoload, so the error hierarchy loads up front
8
+ require_relative "yeptris/error"
9
+ autoload :Document, "yeptris/document"
10
+ autoload :Node, "yeptris/node"
11
+ autoload :YAML, "yeptris/yaml"
12
+ autoload :Materializer, "yeptris/materializer"
13
+ autoload :ValueML, "yeptris/valueml"
14
+ end
15
+
16
+ # Eager native-library resolution (leptris-ruby lesson): fail at
17
+ # require time, not at first parse. The ffi require MUST come after
18
+ # the autoload registrations — ffi.rb opens module Yeptris, and
19
+ # requiring it first would shadow the manifest (leptris-ruby#53).
20
+ begin
21
+ require "yeptris/ffi"
22
+ rescue LoadError => e
23
+ raise LoadError, <<~MSG
24
+ Yeptris could not load the native libyeptris library.
25
+ Set YEPTRIS_LIB_PATH to a libyeptris.{so,dylib,dll}, or use the
26
+ platform gem that vendors it.
27
+ (Underlying error: #{e.message})
28
+ MSG
29
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yeptris
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ description: An FFI-based (no C extension) Ruby YAML library over libyeptris — Psych-compatible
27
+ semantics with libleptris-class performance. The neutral Yeptris::YAML surface ships
28
+ first; the Psych drop-in namespace lands with the recorder-driven Visitors.
29
+ email:
30
+ - open.source@ribose.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.adoc
36
+ - lib/yeptris.rb
37
+ - lib/yeptris/document.rb
38
+ - lib/yeptris/error.rb
39
+ - lib/yeptris/ffi.rb
40
+ - lib/yeptris/materializer.rb
41
+ - lib/yeptris/node.rb
42
+ - lib/yeptris/psych.rb
43
+ - lib/yeptris/psych/coder_shim.rb
44
+ - lib/yeptris/psych/handler.rb
45
+ - lib/yeptris/psych/parser.rb
46
+ - lib/yeptris/psych/visitors.rb
47
+ - lib/yeptris/valueml.rb
48
+ - lib/yeptris/version.rb
49
+ - lib/yeptris/yaml.rb
50
+ homepage: https://github.com/leptris/yeptris
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/leptris/yeptris
55
+ source_code_uri: https://github.com/leptris/yeptris
56
+ changelog_uri: https://github.com/leptris/yeptris/releases
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '3.1'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 4.0.16
72
+ specification_version: 4
73
+ summary: 'The YAML counterpart of libleptris: ultra-performance YAML 1.2 for Ruby'
74
+ test_files: []