teptris 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d38343617b20bb9c4e87ab89b8d60f25b13819deec3d4f9a90fb9b5258e9de61
4
+ data.tar.gz: f80e34bfd48046b0b00f03e052adc51ad7a26623b90ac4cdba99ef7d07d45999
5
+ SHA512:
6
+ metadata.gz: 8c18487e193063df8dcd5406546badc4ba701ce08b473c04dbfdc1d4bbd3117d1f760da328c10575ca73790e231938dc6d393b003d12a53fbb9c48b02e073f18
7
+ data.tar.gz: 10e943823e5e2315c4708ef6e95fad8cde7b4d3a036e06d9bdf9a0a78b1cb29194be5ff63fc1afe8d65e7ebb0894745a1edea96e0706a448573ddad73778dace
@@ -0,0 +1,13 @@
1
+ module Teptris
2
+ class Error < StandardError; end
3
+
4
+ class ParseError < Error
5
+ attr_reader :line, :column
6
+
7
+ def initialize(message, line = 0, column = 0)
8
+ @line = line
9
+ @column = column
10
+ super(message)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,81 @@
1
+ require "ffi"
2
+
3
+ module Teptris
4
+ # Loads the shared libteptris: TEPTRIS_LIB_PATH env, vendored
5
+ # lib/platform/<tag>/, the sibling C checkout's build tree, then the
6
+ # system paths (yeptris-ruby resolution chain).
7
+ module Lib
8
+ extend FFI::Library
9
+
10
+ NAMES = %w[libteptris.dylib libteptris.so libteptris.dll].freeze
11
+
12
+ def self.candidates
13
+ out = []
14
+ out << ENV["TEPTRIS_LIB_PATH"] if ENV["TEPTRIS_LIB_PATH"]
15
+ out.concat(Dir[File.expand_path("../libteptris.*", __dir__)])
16
+ NAMES.each do |n|
17
+ %w[build build-release build-shared build-bench].each do |b|
18
+ out << File.expand_path("../../teptris/#{b}/src/#{n}", __dir__)
19
+ end
20
+ out << n
21
+ end
22
+ out.compact
23
+ end
24
+
25
+ loaded = nil
26
+ candidates.each do |c|
27
+ begin
28
+ ffi_lib c
29
+ loaded = c
30
+ break
31
+ rescue LoadError
32
+ next
33
+ end
34
+ end
35
+ raise Error, "libteptris not found (set TEPTRIS_LIB_PATH)" unless loaded
36
+ LIBRARY_PATH = loaded
37
+
38
+ typedef :pointer, :doc
39
+ typedef :pointer, :node
40
+
41
+ attach_function :teptris_parse, [:pointer, :size_t, :pointer, :pointer], :int
42
+ attach_function :teptris_document_free, [:doc], :void
43
+ attach_function :teptris_document_error, [:doc], :pointer
44
+ attach_function :teptris_document_root, [:doc], :node
45
+ attach_function :teptris_document_emit, [:doc, :pointer, :pointer], :int
46
+ attach_function :teptris_node_kind, [:node], :int
47
+ attach_function :teptris_node_string, [:node, :pointer], :int
48
+ attach_function :teptris_node_integer, [:node, :pointer], :int
49
+ attach_function :teptris_node_float, [:node, :pointer], :int
50
+ attach_function :teptris_node_boolean, [:node, :pointer], :int
51
+ attach_function :teptris_node_datetime, [:node, :pointer], :int
52
+ attach_function :teptris_node_array_length, [:node], :size_t
53
+ attach_function :teptris_node_array_at, [:node, :size_t], :node
54
+ attach_function :teptris_node_table_length, [:node], :size_t
55
+ attach_function :teptris_node_table_at, [:node, :size_t, :pointer], :node
56
+ attach_function :teptris_document_flatten, [:doc, :pointer, :pointer], :int
57
+ attach_function :teptris_flatten_free, [:pointer], :void
58
+
59
+ class ErrorStruct < FFI::Struct
60
+ layout :status, :int, :line, :size_t, :column, :size_t, :message, :string
61
+ end
62
+
63
+ class ViewStruct < FFI::Struct
64
+ layout :ptr, :pointer, :len, :size_t
65
+ end
66
+
67
+ class DateTimeStruct < FFI::Struct
68
+ layout :year, :int32,
69
+ :month, :uint8, :day, :uint8,
70
+ :hour, :uint8, :minute, :uint8, :second, :uint8,
71
+ :_pad, [:uint8, 3],
72
+ :nanosecond, :uint32, :offset_seconds, :int32
73
+ end
74
+ end
75
+
76
+ KIND = {
77
+ string: 0, integer: 1, float: 2, boolean: 3,
78
+ datetime: 4, datetime_local: 5, date_local: 6, time_local: 7,
79
+ array: 8, table: 9
80
+ }.freeze
81
+ end
@@ -0,0 +1,313 @@
1
+ require "date"
2
+
3
+ require_relative "error"
4
+ require_relative "lib"
5
+ require_relative "version"
6
+
7
+ module Teptris
8
+ # API shape mirrors the tomlib gem: load/dump, String keys, and the
9
+ # tomlib datetime contract (Time with zone for offset datetimes, local
10
+ # Time for local datetimes, Date for dates, String "HH:MM:SS" for
11
+ # local times).
12
+ module TOML
13
+ class << self
14
+ def load(toml)
15
+ parse_and_materialize(toml, 0, nil)
16
+ end
17
+
18
+ # Safe-load shaped entry point for frameworks: permitted_classes
19
+ # restricts datetime materialization (raise when a value would
20
+ # materialize an unpermitted class); datetime_policy :string keeps
21
+ # every datetime kind as its canonical ISO string instead.
22
+ def safe_load(toml, permitted_classes: nil, datetime_policy: :native)
23
+ unless %i[native string].include?(datetime_policy)
24
+ raise ArgumentError, "datetime_policy must be :native or :string"
25
+ end
26
+ mode = datetime_policy == :string ? 1 : 0
27
+ permitted = permitted_classes && Set.new(permitted_classes)
28
+ if permitted && mode.zero?
29
+ permitted << String # TOML scalars are always materialized
30
+ end
31
+ parse_and_materialize(toml, mode, permitted)
32
+ end
33
+
34
+ # Reserved: fused descriptor materialization, the same descriptor
35
+ # shape as leptris/yeptris#238 — see docs/DESCRIPTOR_ABI.md. The
36
+ # entry point exists so frameworks can code against it today; the
37
+ # fused native pass lands with the co-designed ABI.
38
+ def load_schema(toml, descriptor)
39
+ raise Error,
40
+ "descriptor materialization lands with the co-designed " \
41
+ "ABI (leptris/yeptris#238); entry point is reserved — " \
42
+ "see docs/DESCRIPTOR_ABI.md (descriptor #{descriptor.class})"
43
+ end
44
+
45
+ private
46
+
47
+ def parse_and_materialize(toml, mode, permitted)
48
+ raise ArgumentError, "input must be a String" unless toml.is_a?(String)
49
+
50
+ data = FFI::MemoryPointer.from_string(toml)
51
+ docp = FFI::MemoryPointer.new(:pointer)
52
+ st = Lib.teptris_parse(data, toml.bytesize, nil, docp)
53
+ handle = docp.read_pointer
54
+ if st != Teptris::TEPTRIS_OK
55
+ raise build_parse_error(handle)
56
+ end
57
+ begin
58
+ @mode = mode
59
+ @permitted = permitted
60
+ load_flat(handle)
61
+ ensure
62
+ @mode = nil
63
+ @permitted = nil
64
+ Lib.teptris_document_free(handle)
65
+ end
66
+ end
67
+
68
+ public
69
+
70
+ def load_file(path)
71
+ load(File.read(path))
72
+ end
73
+
74
+ def dump(obj)
75
+ raise ArgumentError, "cannot dump #{obj.class}" unless obj.is_a?(Hash)
76
+
77
+ out = +""
78
+ emit_table(obj, out, nil)
79
+ out
80
+ end
81
+
82
+ private
83
+
84
+ def build_parse_error(handle)
85
+ line = 0
86
+ column = 0
87
+ msg = "parse failed"
88
+ unless handle.null?
89
+ es = Lib::ErrorStruct.new(Lib.teptris_document_error(handle))
90
+ line = es[:line]
91
+ column = es[:column]
92
+ msg = es[:message]
93
+ end
94
+ ParseError.new(msg, line, column)
95
+ end
96
+
97
+ # One-bulk-drain load: the C side flattens the whole tree in a
98
+ # single crossing; this decoder walks the typed flat buffer.
99
+ FLAT_TABLE = 0x01
100
+ FLAT_ARRAY = 0x02
101
+ FLAT_STRING = 0x03
102
+ FLAT_INT = 0x04
103
+ FLAT_FLOAT = 0x05
104
+ FLAT_FALSE = 0x06
105
+ FLAT_TRUE = 0x07
106
+ FLAT_DT = 0x08 # ..0x0B: offset, local-datetime, date, local-time
107
+
108
+ def load_flat(handle)
109
+ bufp = FFI::MemoryPointer.new(:pointer)
110
+ lenp = FFI::MemoryPointer.new(:size_t)
111
+ st = Lib.teptris_document_flatten(handle, bufp, lenp)
112
+ raise Error, "flatten failed" unless st == Teptris::TEPTRIS_OK
113
+
114
+ buf = bufp.read_pointer
115
+ begin
116
+ data = buf.get_bytes(0, lenp.read_uint64)
117
+ value, = decode_flat(data, 0)
118
+ value
119
+ ensure
120
+ Lib.teptris_flatten_free(buf)
121
+ end
122
+ end
123
+
124
+ def decode_flat(d, pos)
125
+ case d.getbyte(pos)
126
+ when FLAT_TABLE
127
+ n = d.unpack1("L<", offset: pos + 1)
128
+ pos += 5
129
+ h = {}
130
+ n.times do
131
+ kl = d.unpack1("L<", offset: pos)
132
+ pos += 4
133
+ k = d.byteslice(pos, kl).force_encoding(Encoding::UTF_8)
134
+ pos += kl
135
+ v, pos = decode_flat(d, pos)
136
+ h[k] = v
137
+ end
138
+ [h, pos]
139
+ when FLAT_ARRAY
140
+ n = d.unpack1("L<", offset: pos + 1)
141
+ pos += 5
142
+ a = Array.new(n)
143
+ n.times do |i|
144
+ a[i], pos = decode_flat(d, pos)
145
+ end
146
+ [a, pos]
147
+ when FLAT_STRING
148
+ l = d.unpack1("L<", offset: pos + 1)
149
+ pos += 5
150
+ [d.byteslice(pos, l).force_encoding(Encoding::UTF_8), pos + l]
151
+ when FLAT_INT
152
+ [d.unpack1("q<", offset: pos + 1), pos + 9]
153
+ when FLAT_FLOAT
154
+ [d.unpack1("E", offset: pos + 1), pos + 9]
155
+ when FLAT_FALSE then [false, pos + 1]
156
+ when FLAT_TRUE then [true, pos + 1]
157
+ when FLAT_DT, FLAT_DT + 1, FLAT_DT + 2, FLAT_DT + 3
158
+ kind = d.getbyte(pos)
159
+ year = d.unpack1("q<", offset: pos + 1)
160
+ mon = d.getbyte(pos + 9)
161
+ day = d.getbyte(pos + 10)
162
+ hour = d.getbyte(pos + 11)
163
+ min = d.getbyte(pos + 12)
164
+ sec = d.getbyte(pos + 13)
165
+ nsec = d.unpack1("L<", offset: pos + 14)
166
+ off = d.unpack1("q<", offset: pos + 18)
167
+ [flat_datetime(kind, year, mon, day, hour, min, sec, nsec, off),
168
+ pos + 26]
169
+ else
170
+ raise Error, "corrupt flat buffer at #{pos}"
171
+ end
172
+ end
173
+
174
+ def flat_datetime(kind, y, mon, day, hour, min, sec, nsec, off)
175
+ return flat_datetime_string(kind, y, mon, day, hour, min, sec, nsec, off) if @mode == 1
176
+
177
+ rational = Rational(sec * 1_000_000_000 + nsec, 1_000_000_000)
178
+ case kind
179
+ when FLAT_DT
180
+ check_permitted!(Time)
181
+ Time.new(y, mon, day, hour, min, rational, off)
182
+ when FLAT_DT + 1
183
+ check_permitted!(Time)
184
+ Time.local(y, mon, day, hour, min, rational)
185
+ when FLAT_DT + 2
186
+ check_permitted!(Date)
187
+ Date.new(y, mon, day)
188
+ else
189
+ s = format("%02d:%02d:%02d", hour, min, sec)
190
+ s << "." << format("%09d", nsec).sub(/0+\z/, "") if nsec.positive?
191
+ s
192
+ end
193
+ end
194
+
195
+ def check_permitted!(klass)
196
+ permitted = @permitted
197
+ return if permitted.nil? || permitted.include?(klass)
198
+
199
+ raise Error,
200
+ "value materializes #{klass}, which is not in " \
201
+ "permitted_classes (use datetime_policy: :string to keep " \
202
+ "datetimes as strings)"
203
+ end
204
+
205
+ def flat_datetime_string(kind, y, mon, day, hour, min, sec, nsec, off)
206
+ case kind
207
+ when FLAT_DT + 2 then return format("%04d-%02d-%02d", y, mon, day)
208
+ when FLAT_DT + 3 then s = +""
209
+ else s = +(format("%04d-%02d-%02dT", y, mon, day))
210
+ end
211
+ s << format("%02d:%02d:%02d", hour, min, sec)
212
+ if nsec.positive?
213
+ s << "." << format("%09d", nsec).sub(/0+\z/, "")
214
+ end
215
+ case kind
216
+ when FLAT_DT
217
+ off.zero? ? s << "Z" : s << format("%+03d:%02d", off / 3600, (off % 3600).abs / 60)
218
+ end
219
+ s
220
+ end
221
+
222
+ # ---- dump -----------------------------------------------------------
223
+
224
+ def emit_table(hash, out, prefix)
225
+ hash.each do |k, v|
226
+ next if table?(v) || aot?(v)
227
+
228
+ out << emit_key(k) << " = " << emit_value(v) << "\n"
229
+ end
230
+ hash.each do |k, v|
231
+ next unless table?(v) || aot?(v)
232
+
233
+ path = prefix ? "#{prefix}.#{emit_key(k)}" : emit_key(k)
234
+ if aot?(v)
235
+ v.each do |item|
236
+ out << "[[" << path << "]]\n"
237
+ emit_table(item, out, path)
238
+ end
239
+ else
240
+ out << "[" << path << "]\n"
241
+ emit_table(v, out, path)
242
+ end
243
+ end
244
+ end
245
+
246
+ def table?(v)
247
+ v.is_a?(Hash)
248
+ end
249
+
250
+ def aot?(v)
251
+ v.is_a?(Array) && !v.empty? && v.all? { |e| e.is_a?(Hash) }
252
+ end
253
+
254
+ def emit_key(k)
255
+ k = k.to_s
256
+ if /\A[A-Za-z0-9_-]+\z/.match?(k)
257
+ k
258
+ elsif !k.include?("'") && !k.each_char.any? { |c| c.ord < 0x20 }
259
+ "'#{k}'"
260
+ else
261
+ "\"#{escape_basic(k)}\""
262
+ end
263
+ end
264
+
265
+ BASIC_ESCAPES = {
266
+ "\\" => "\\\\\\\\", # \ -> \\
267
+ "\"" => "\\\"", "\b" => "\\b", "\f" => "\\f",
268
+ "\n" => "\\n", "\r" => "\\r", "\t" => "\\t"
269
+ }.freeze
270
+
271
+ def escape_basic(s)
272
+ s.gsub(/[\\\"\b\f\n\r\t]/) { |c| BASIC_ESCAPES[c] }
273
+ end
274
+
275
+ def emit_value(v)
276
+ case v
277
+ when String
278
+ if !v.include?("'") && !v.each_char.any? { |c| c.ord < 0x20 }
279
+ "'#{v}'"
280
+ else
281
+ "\"#{escape_basic(v)}\""
282
+ end
283
+ when Integer then v.to_s
284
+ when Float
285
+ return v.to_s if v.finite?
286
+
287
+ v.nan? ? "nan" : (v.positive? ? "inf" : "-inf")
288
+ when TrueClass then "true"
289
+ when FalseClass then "false"
290
+ when Time
291
+ base = v.strftime("%Y-%m-%dT%H:%M:%S")
292
+ frac = v.strftime("%N").sub(/0+\z/, "")
293
+ base << "." << frac unless frac.empty?
294
+ base << tz(v)
295
+ when Date then v.iso8601
296
+ when Array then "[#{v.map { |e| emit_value(e) }.join(', ')}]"
297
+ when Hash
298
+ "{#{v.map { |k, e| "#{emit_key(k)} = #{emit_value(e)}" }.join(', ')}}"
299
+ else raise Error, "cannot dump #{v.class}"
300
+ end
301
+ end
302
+
303
+ def tz(time)
304
+ off = time.utc_offset
305
+ return "Z" if off.zero?
306
+
307
+ sign = off.negative? ? "-" : "+"
308
+ off = -off if off.negative?
309
+ format("%s%02d:%02d", sign, off / 3600, (off % 3600) / 60)
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,3 @@
1
+ module Teptris
2
+ VERSION = "0.1.0"
3
+ end
data/lib/teptris.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "set"
2
+
3
+ require_relative "teptris/version"
4
+ require_relative "teptris/error"
5
+ require_relative "teptris/lib"
6
+ require_relative "teptris/toml"
7
+
8
+ module Teptris
9
+ TEPTRIS_OK = 0
10
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: teptris
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - teptris contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ description: An FFI binding over libteptris, a C11 TOML 1.0 parser/emitter. API shape
28
+ mirrors the tomlib gem.
29
+ email:
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/teptris.rb
35
+ - lib/teptris/error.rb
36
+ - lib/teptris/lib.rb
37
+ - lib/teptris/toml.rb
38
+ - lib/teptris/version.rb
39
+ homepage: https://github.com/leptris/teptris-ruby
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.22
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: TOML for Ruby at libleptris speed (FFI, no C extension)
62
+ test_files: []