soml-lang 0.0.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.
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOML
4
+ # The writer. It walks the value once and appends to one string, so the time is linear in the output however deep the nesting. It writes canonical form, except that members keep the order of their hash unless `canonical` is true.
5
+ #
6
+ # @api private
7
+ class Generator
8
+ # A byte that cannot be in a `'...'` string: a control character, an apostrophe, or DEL. A string with one is written as `"..."`. The set is a byte table, because a scan of it is faster than the regex it replaces.
9
+ NEEDS_ESCAPED_BYTE = Array.new(256) { |byte| byte < 0x20 || byte == 0x27 || byte == 0x7F }.freeze
10
+
11
+ ESCAPED_CHARACTER = /[\x00-\x1F"\\\x7F]/
12
+
13
+ # Shareable, so that a ractor can read it.
14
+ ESCAPES = Ractor.make_shareable(
15
+ [*(0x00..0x1F), 0x7F].to_h { |code| [code.chr, format('\\u{%x}', code)] }.merge(
16
+ '"' => '\\"',
17
+ '\\' => '\\\\',
18
+ "\n" => '\\n',
19
+ "\t" => '\\t'
20
+ )
21
+ )
22
+
23
+ private_constant :NEEDS_ESCAPED_BYTE, :ESCAPED_CHARACTER, :ESCAPES
24
+
25
+ # @param canonical [Boolean] Whether members are sorted by key, as in canonical form, rather than kept in the order of their hash.
26
+ # @param one_line [Boolean] Whether a container is written on one line, as in `[1, {a: 2}]`, for {Editor} to write a value inside a container that is on one line.
27
+ def initialize(canonical: false, one_line: false)
28
+ @canonical = canonical
29
+ @one_line = one_line
30
+ @line_break = one_line ? '' : "\n"
31
+ @buffer = +''
32
+ @ancestors = {}.compare_by_identity
33
+ end
34
+
35
+ # @param value [Hash, Array]
36
+ # @return [String]
37
+ def generate(value)
38
+ case value
39
+ when Hash
40
+ enter(value, 1)
41
+
42
+ # A brace-less object needs at least one entry, so the empty object keeps its braces.
43
+ return +"{}\n" if value.empty?
44
+
45
+ write_members(value, 1, '')
46
+ when Array
47
+ write_container(value, 1, '')
48
+ @buffer << "\n"
49
+ else
50
+ raise GenerateError, "The top-level value must be a Hash or an Array, because a document is a collection, not #{describe(value)}"
51
+ end
52
+
53
+ @buffer
54
+ end
55
+
56
+ # One value, for {Editor}. A container's own depth is `depth`, and its lines are indented as if it were at the top level.
57
+ #
58
+ # @param value [Object]
59
+ # @param depth [Integer]
60
+ # @return [String]
61
+ def generate_value(value, depth)
62
+ write_value(value, depth, '')
63
+ @buffer
64
+ end
65
+
66
+ # The key as a UTF-8 string, checked outside the block that writes its member, so that an invalid key is reported at the path of its object. {Editor} reads the keys of a path with it too.
67
+ #
68
+ # @param key [String, Symbol]
69
+ # @param written [Boolean] Whether the key is written. A key that is only looked for, as in the path of a removal, may have a carriage return, because no document has such a key, so it finds nothing.
70
+ # @return [String]
71
+ def member_name(key, written: true)
72
+ name = case key
73
+ when String
74
+ to_utf8(key, 'key')
75
+ when Symbol
76
+ to_utf8(key.name, 'key')
77
+ else
78
+ # Only an `Integer` or a `Float` is shown, as in `an Integer (1)`, because `nil`, `true`, and `false` are named already. Anything else, such as an array, or a `Numeric` subclass with its instance variables, can be too deep for `inspect`, which then overflows the machine stack and can crash the process rather than raise, and a `BasicObject` has no `inspect`.
79
+ shown = Integer === key || Float === key ? " (#{key.inspect})" : ''
80
+
81
+ raise GenerateError, "A key must be a String or a Symbol, not #{describe(key)}#{shown}, because a key is never converted"
82
+ end
83
+
84
+ raise GenerateError, "Cannot generate the key #{Parser.describe_path([name])}, because it contains a carriage return (U+000D), which is not representable" if written && name.include?("\r")
85
+
86
+ name
87
+ end
88
+
89
+ private
90
+
91
+ # Writes a value whose own depth, if it is a container, is `depth`, and whose lines after the first are indented by `indent`.
92
+ def write_value(value, depth, indent)
93
+ case value
94
+ when String
95
+ write_string(value)
96
+ when Integer
97
+ raise GenerateError, "Cannot generate the integer #{value}, because it is outside the 64-bit range" unless value.between?(INT64_MIN, INT64_MAX)
98
+
99
+ @buffer << value.to_s
100
+ when Hash, Array
101
+ write_container(value, depth, indent)
102
+ when true
103
+ @buffer << 'true'
104
+ when false
105
+ @buffer << 'false'
106
+ when nil
107
+ @buffer << 'null'
108
+ when Float
109
+ @buffer << format_float(value)
110
+ when Symbol
111
+ write_string(value.name)
112
+ when Time
113
+ @buffer << format_instant(value)
114
+ when Duration
115
+ @buffer << value.to_s
116
+ else
117
+ raise GenerateError, describe_unsupported(value)
118
+ end
119
+ end
120
+
121
+ # `@ancestors` holds only the containers on the way to this one, so the same array in two places is written twice, and only an array inside itself is circular.
122
+ def enter(value, depth)
123
+ raise GenerateError, "Cannot generate a value nested more than #{MAX_DEPTH} levels deep" if depth > MAX_DEPTH
124
+ raise GenerateError, 'Cannot generate a circular structure' if @ancestors.key?(value)
125
+
126
+ @ancestors[value] = true
127
+ end
128
+
129
+ def write_container(value, depth, indent)
130
+ enter(value, depth)
131
+ inner_indent = INDENTS[indent.length + 1]
132
+
133
+ if value.is_a?(Array)
134
+ write_array(value, depth, indent, inner_indent)
135
+ else
136
+ write_hash(value, depth, indent, inner_indent)
137
+ end
138
+
139
+ @ancestors.delete(value)
140
+ end
141
+
142
+ def write_array(array, depth, indent, inner_indent)
143
+ return @buffer << '[]' if array.empty?
144
+
145
+ @buffer << '[' << @line_break
146
+ index = 0
147
+
148
+ while index < array.length
149
+ @buffer << item_prefix(index, inner_indent)
150
+
151
+ begin
152
+ write_value(array[index], depth + 1, inner_indent)
153
+ rescue GenerateError => error
154
+ error.path.unshift(index)
155
+ raise
156
+ end
157
+
158
+ @buffer << @line_break
159
+ index += 1
160
+ end
161
+
162
+ @buffer << indent unless @one_line
163
+ @buffer << ']'
164
+ end
165
+
166
+ def write_hash(hash, depth, indent, inner_indent)
167
+ return @buffer << '{}' if hash.empty?
168
+
169
+ @buffer << '{' << @line_break
170
+ write_members(hash, depth, inner_indent)
171
+ @buffer << indent unless @one_line
172
+ @buffer << '}'
173
+ end
174
+
175
+ # Writes each member of `hash`, whose own depth is `depth`, on its own line, indented by `indent`, or after a comma on one line.
176
+ def write_members(hash, depth, indent)
177
+ index = 0
178
+
179
+ each_member(hash) do |key, member|
180
+ @buffer << item_prefix(index, indent)
181
+ index += 1
182
+ write_key(key)
183
+ @buffer << ': '
184
+ write_value(member, depth + 1, indent)
185
+ @buffer << @line_break
186
+ rescue GenerateError => error
187
+ error.path.unshift(key)
188
+ raise
189
+ end
190
+ end
191
+
192
+ # What comes before the member or item at `index` in a container whose members or items are indented by `indent`: a comma and a space after the first one on one line, and otherwise the indentation of a new line.
193
+ def item_prefix(index, indent)
194
+ return indent unless @one_line
195
+
196
+ index.zero? ? '' : ', '
197
+ end
198
+
199
+ # Yields each member with its key as a UTF-8 string, in the order of the hash, or sorted by key for canonical form.
200
+ def each_member(hash)
201
+ keys = hash.keys
202
+ sort_keys!(keys) if @canonical
203
+ names = {}
204
+
205
+ keys.each do |key|
206
+ name = member_name(key)
207
+
208
+ # Two keys that are the same string, such as `:a` and `'a'`, would be a duplicate key.
209
+ raise GenerateError, "Cannot generate the key #{Parser.describe_path([name])} twice, because two keys, such as :a and 'a', are the same string" if names.key?(name)
210
+
211
+ names[name] = true
212
+ yield name, hash[key]
213
+ end
214
+ end
215
+
216
+ # Sorts the keys by their UTF-8 names. UTF-8 byte order is code point order, which is the canonical order.
217
+ def sort_keys!(keys)
218
+ # The common cases, all keys strings or all symbols in UTF-8 or US-ASCII, sort the keys themselves, because then their byte order is the order of their UTF-8 names. A string from `Symbol#to_s` is in US-ASCII. Anything else, such as a symbol in ISO-8859-1, sorts the keys by their UTF-8 names.
219
+ if keys.all? { |key| String === key && utf8_compatible?(key) } || keys.all? { |key| Symbol === key && utf8_compatible?(key) }
220
+ keys.sort!
221
+ else
222
+ keys.replace(keys.map { |key| [member_name(key), key] }.sort_by!(&:first).map!(&:last))
223
+ end
224
+ end
225
+
226
+ def utf8_compatible?(key)
227
+ key.encoding == Encoding::UTF_8 || key.encoding == Encoding::US_ASCII
228
+ end
229
+
230
+ # A key that `member_name` checked, so it is valid UTF-8 without a carriage return.
231
+ def write_key(key)
232
+ if SOML.bare_key?(key)
233
+ @buffer << key
234
+ else
235
+ write_string(key)
236
+ end
237
+ end
238
+
239
+ def write_string(string)
240
+ string = to_utf8(string, 'string')
241
+
242
+ # One scan settles the common case: no quote and no control character.
243
+ index = 0
244
+ length = string.bytesize
245
+
246
+ while index < length && !NEEDS_ESCAPED_BYTE[string.getbyte(index)]
247
+ index += 1
248
+ end
249
+
250
+ if index == length
251
+ @buffer << "'" << string << "'"
252
+ return
253
+ end
254
+
255
+ raise GenerateError, 'Cannot generate a string containing a carriage return (U+000D), which is not representable' if string.include?("\r")
256
+
257
+ @buffer << '"' << string.gsub(ESCAPED_CHARACTER, ESCAPES) << '"'
258
+ end
259
+
260
+ def to_utf8(string, kind)
261
+ case string.encoding
262
+ when Encoding::UTF_8
263
+ # Already UTF-8.
264
+ when Encoding::BINARY, Encoding::US_ASCII
265
+ # Bytes are read as UTF-8, as `SOML.parse` reads them. A US-ASCII string often holds other bytes, such as a file read with `LANG=C`.
266
+ string = string.dup.force_encoding(Encoding::UTF_8) unless string.ascii_only?
267
+ else
268
+ begin
269
+ string = string.encode(Encoding::UTF_8)
270
+ rescue EncodingError => error
271
+ raise GenerateError, "Cannot generate a #{kind} that cannot be converted to UTF-8: #{error.message}"
272
+ end
273
+ end
274
+
275
+ raise GenerateError, "Cannot generate a #{kind} that is not valid #{string.encoding}" unless string.valid_encoding?
276
+
277
+ string
278
+ end
279
+
280
+ # The shortest decimal that reads back as the same float, laid out as ECMAScript `Number::toString` does, with `.0` added when there is neither a fraction nor an exponent, so that a float never reads back as an int.
281
+ def format_float(value)
282
+ raise GenerateError, 'NaN is not representable. Use nil for a missing value' if value.nan?
283
+ return value.positive? ? 'infinity' : '-infinity' if value.infinite?
284
+
285
+ # Covers -0.0 too, since zero has one value whatever its sign.
286
+ return '0.0' if value.zero?
287
+
288
+ # `Float#to_s` gives the shortest digits that read back as the same float. Without an exponent, which it uses below 0.0001 and from 1e16, its layout is already canonical.
289
+ text = value.to_s
290
+ return text unless text.include?('e')
291
+
292
+ mantissa, exponent = text.delete_prefix('-').split('e')
293
+ whole, fraction = mantissa.split('.')
294
+ digits = "#{whole}#{fraction}".sub(/0+\z/, '')
295
+ length = digits.length
296
+
297
+ # The decimal point is after `point` digits, so the value is 0.digits × 10^point.
298
+ point = exponent.to_i + whole.length
299
+
300
+ body = if length <= point && point <= 21
301
+ "#{digits}#{'0' * (point - length)}.0"
302
+ # `Float#to_s` only uses an exponent when the point is outside the digits, so this step of the spec is not reached today. It stays, so that the layout is the spec's whatever `to_s` does.
303
+ elsif point.positive? && point < length
304
+ "#{digits[0, point]}.#{digits[point..]}"
305
+ elsif point > -6 && point <= 0
306
+ "0.#{'0' * -point}#{digits}"
307
+ else
308
+ "#{digits[0]}#{length > 1 ? ".#{digits[1..]}" : ''}e#{point - 1}"
309
+ end
310
+
311
+ value.negative? ? "-#{body}" : body
312
+ end
313
+
314
+ def format_instant(time)
315
+ utc = time.getutc
316
+ nanoseconds = utc.nsec
317
+
318
+ # A time made from a float, such as `Time.at(Time.now.to_f)`, has the float's binary fraction.
319
+ raise GenerateError, "Cannot generate the instant #{time.inspect}, because it has a fraction of a second finer than a nanosecond. Round it to nanoseconds with time.round(9)" unless utc.subsec * 1_000_000_000 == nanoseconds
320
+ raise GenerateError, "Cannot generate the instant #{time.inspect}, because in UTC it falls outside the years 0001 to 9999" unless utc.year.between?(1, 9999)
321
+
322
+ text = format('%04d-%02d-%02dT%02d:%02d:%02d', utc.year, utc.month, utc.day, utc.hour, utc.min, utc.sec)
323
+
324
+ # The fraction is nine digits with the trailing zeros removed.
325
+ text << ".#{format('%09d', nanoseconds).sub(/0+\z/, '')}" if nanoseconds.positive?
326
+ text << 'Z'
327
+ end
328
+
329
+ # The class is read with `Kernel#class`, which a `BasicObject` does not have. An anonymous class, such as one from `Data.define`, is named by its nearest named superclass.
330
+ def describe(value)
331
+ case value
332
+ when nil, true, false
333
+ value.inspect
334
+ else
335
+ type = Kernel.instance_method(:class).bind_call(value)
336
+ named = type
337
+ named = named.superclass until named.name
338
+ name = Parser.lossy_utf8(named.equal?(type) ? type.name : "anonymous #{named.name}")
339
+ "#{name.match?(/\A[AEIOUaeiou]/) ? 'an' : 'a'} #{name}"
340
+ end
341
+ end
342
+
343
+ def describe_unsupported(value)
344
+ if defined?(::DateTime) && ::DateTime === value
345
+ 'Cannot generate a DateTime. Convert it to a Time with to_time'
346
+ elsif defined?(::Date) && ::Date === value
347
+ "Cannot generate a Date, because a date is not an instant. Write it as a String, such as '#{value}'"
348
+ elsif defined?(::ActiveSupport::Duration) && ::ActiveSupport::Duration === value
349
+ # Rails code writes `5.minutes`. Its days, months, and years follow the calendar, so it is not converted.
350
+ "Cannot generate an ActiveSupport::Duration, because its days, months, and years are not a fixed length. Use a SOML::Duration, such as SOML::Duration.parse('5m')"
351
+ elsif Numeric === value
352
+ "Cannot generate #{describe(value)}. Convert it to an Integer or a Float"
353
+ elsif Struct === value || Data === value
354
+ "Cannot generate #{describe(value)}. Convert it to a Hash with to_h"
355
+ elsif Set === value
356
+ "Cannot generate #{describe(value)}. Convert it to an Array with to_a"
357
+ elsif Pathname === value
358
+ "Cannot generate #{describe(value)}. Convert it to a String with to_s"
359
+ else
360
+ "Cannot generate #{describe(value)}. Only Hash, Array, String, Symbol, Integer, Float, true, false, nil, Time, and SOML::Duration can be generated"
361
+ end
362
+ end
363
+ end
364
+ end