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.
- checksums.yaml +7 -0
- data/README.adoc +59 -0
- data/lib/yeptris/document.rb +209 -0
- data/lib/yeptris/error.rb +19 -0
- data/lib/yeptris/ffi.rb +216 -0
- data/lib/yeptris/materializer.rb +338 -0
- data/lib/yeptris/node.rb +320 -0
- data/lib/yeptris/psych/coder_shim.rb +55 -0
- data/lib/yeptris/psych/handler.rb +90 -0
- data/lib/yeptris/psych/parser.rb +105 -0
- data/lib/yeptris/psych/visitors.rb +364 -0
- data/lib/yeptris/psych.rb +315 -0
- data/lib/yeptris/valueml.rb +218 -0
- data/lib/yeptris/version.rb +5 -0
- data/lib/yeptris/yaml.rb +247 -0
- data/lib/yeptris.rb +29 -0
- metadata +74 -0
data/lib/yeptris/yaml.rb
ADDED
|
@@ -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: []
|