cton 0.1.0 → 0.1.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.
data/lib/cton.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "bigdecimal"
4
4
  require_relative "cton/version"
5
+ require_relative "cton/encoder"
6
+ require_relative "cton/decoder"
5
7
 
6
8
  module Cton
7
9
  class Error < StandardError; end
@@ -20,562 +22,5 @@ module Cton
20
22
  Decoder.new(symbolize_names: symbolize_names).decode(cton_string)
21
23
  end
22
24
  alias parse load
23
-
24
- class Encoder
25
- SAFE_TOKEN = /\A[0-9A-Za-z_.:-]+\z/.freeze
26
- NUMERIC_TOKEN = /\A-?(?:\d+)(?:\.\d+)?(?:[eE][+-]?\d+)?\z/.freeze
27
- RESERVED_LITERALS = %w[true false null].freeze
28
-
29
- def initialize(separator: "\n")
30
- @separator = separator || ""
31
- end
32
-
33
- def encode(payload)
34
- encode_root(payload)
35
- end
36
-
37
- private
38
-
39
- attr_reader :separator
40
-
41
- def encode_root(value)
42
- case value
43
- when Hash
44
- value.map { |key, nested| encode_top_level_pair(key, nested) }.join(separator)
45
- else
46
- encode_value(value, context: :standalone)
47
- end
48
- end
49
-
50
- def encode_top_level_pair(key, value)
51
- "#{format_key(key)}#{encode_value(value, context: :top_pair)}"
52
- end
53
-
54
- def encode_value(value, context:)
55
- case value
56
- when Hash
57
- encode_object(value)
58
- when Array
59
- encode_array(value)
60
- else
61
- prefix = context == :top_pair ? "=" : ""
62
- "#{prefix}#{encode_scalar(value)}"
63
- end
64
- end
65
-
66
- def encode_object(hash)
67
- return "()" if hash.empty?
68
-
69
- pairs = hash.map do |key, value|
70
- "#{format_key(key)}=#{encode_value(value, context: :object)}"
71
- end
72
- "(#{pairs.join(',')})"
73
- end
74
-
75
- def encode_array(list)
76
- length = list.length
77
- return "[0]=" if length.zero?
78
-
79
- if table_candidate?(list)
80
- "[#{length}]#{encode_table(list)}"
81
- else
82
- body = if list.all? { |value| scalar?(value) }
83
- list.map { |value| encode_scalar(value) }.join(",")
84
- else
85
- list.map { |value| encode_array_element(value) }.join(",")
86
- end
87
- "[#{length}]=#{body}"
88
- end
89
- end
90
-
91
- def encode_table(rows)
92
- header = rows.first.keys
93
- header_token = "{#{header.map { |key| format_key(key) }.join(',')}}"
94
- table_rows = rows.map do |row|
95
- header.map { |field| encode_scalar(row.fetch(field)) }.join(",")
96
- end
97
- "#{header_token}=#{table_rows.join(';')}"
98
- end
99
-
100
- def encode_array_element(value)
101
- encode_value(value, context: :array)
102
- end
103
-
104
- def encode_scalar(value)
105
- case value
106
- when String
107
- encode_string(value)
108
- when TrueClass, FalseClass
109
- value ? "true" : "false"
110
- when NilClass
111
- "null"
112
- when Numeric
113
- format_number(value)
114
- else
115
- raise EncodeError, "Unsupported value: #{value.class}"
116
- end
117
- end
118
-
119
- def encode_string(value)
120
- return '""' if value.empty?
121
-
122
- string_needs_quotes?(value) ? quote_string(value) : value
123
- end
124
-
125
- FLOAT_DECIMAL_PRECISION = Float::DIG
126
-
127
- def format_number(value)
128
- case value
129
- when Float
130
- return "null" if value.nan? || value.infinite?
131
-
132
- normalize_decimal_string(float_decimal_string(value))
133
- when Integer
134
- value.to_s
135
- else
136
- if defined?(BigDecimal) && value.is_a?(BigDecimal)
137
- normalize_decimal_string(value.to_s("F"))
138
- else
139
- value.to_s
140
- end
141
- end
142
- end
143
-
144
- def normalize_decimal_string(string)
145
- stripped = string.start_with?("+") ? string[1..-1] : string
146
- return "0" if zero_string?(stripped)
147
-
148
- if stripped.include?(".")
149
- stripped = stripped.sub(/0+\z/, "")
150
- stripped = stripped.sub(/\.\z/, "")
151
- end
152
-
153
- stripped
154
- end
155
-
156
- def zero_string?(string)
157
- string.match?(/\A-?0+(?:\.0+)?\z/)
158
- end
159
-
160
- def float_decimal_string(value)
161
- if defined?(BigDecimal)
162
- BigDecimal(value.to_s).to_s("F")
163
- else
164
- Kernel.format("%.#{FLOAT_DECIMAL_PRECISION}f", value)
165
- end
166
- end
167
-
168
- def format_key(key)
169
- key_string = key.to_s
170
- unless SAFE_TOKEN.match?(key_string)
171
- raise EncodeError, "Invalid key: #{key_string.inspect}"
172
- end
173
- key_string
174
- end
175
-
176
- def string_needs_quotes?(value)
177
- return true unless SAFE_TOKEN.match?(value)
178
- RESERVED_LITERALS.include?(value) || numeric_like?(value)
179
- end
180
-
181
- def numeric_like?(value)
182
- NUMERIC_TOKEN.match?(value)
183
- end
184
-
185
- def quote_string(value)
186
- "\"#{escape_string(value)}\""
187
- end
188
-
189
- def escape_string(value)
190
- value.gsub(/["\\\n\r\t]/) do |char|
191
- case char
192
- when "\n" then "\\n"
193
- when "\r" then "\\r"
194
- when "\t" then "\\t"
195
- else
196
- "\\#{char}"
197
- end
198
- end
199
- end
200
-
201
- def scalar?(value)
202
- value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.nil?
203
- end
204
-
205
- def table_candidate?(rows)
206
- return false if rows.empty?
207
-
208
- first = rows.first
209
- return false unless first.is_a?(Hash) && !first.empty?
210
-
211
- keys = first.keys
212
- rows.all? do |row|
213
- row.is_a?(Hash) && row.keys == keys && row.values.all? { |val| scalar?(val) }
214
- end
215
- end
216
- end
217
-
218
- class Decoder
219
- TERMINATORS = [",", ";", ")", "]", "}"].freeze
220
-
221
- def initialize(symbolize_names: false)
222
- @symbolize_names = symbolize_names
223
- end
224
-
225
- def decode(cton)
226
- @source = cton.to_s
227
- @index = 0
228
- skip_ws
229
-
230
- value = if key_ahead?(@index)
231
- parse_document
232
- else
233
- parse_value(allow_key_boundary: true)
234
- end
235
-
236
- skip_ws
237
- raise ParseError, "Unexpected trailing data" unless eof?
238
-
239
- value
240
- end
241
-
242
- private
243
-
244
- attr_reader :symbolize_names
245
-
246
- def parse_document
247
- result = {}
248
- until eof?
249
- key = parse_key_name
250
- value = parse_value_for_key
251
- assign_pair(result, key, value)
252
- skip_ws
253
- end
254
- result
255
- end
256
-
257
- def parse_value_for_key
258
- skip_ws
259
- char = current_char
260
- case char
261
- when "("
262
- parse_object
263
- when "["
264
- parse_array
265
- when "="
266
- advance
267
- parse_scalar(allow_key_boundary: true)
268
- else
269
- raise ParseError, "Unexpected token #{char.inspect} while reading value"
270
- end
271
- end
272
-
273
- def parse_object
274
- expect!("(")
275
- skip_ws
276
- if current_char == ")"
277
- expect!(")")
278
- return {}
279
- end
280
-
281
- pairs = {}
282
- loop do
283
- key = parse_key_name
284
- expect!("=")
285
- value = parse_value
286
- assign_pair(pairs, key, value)
287
- skip_ws
288
- break if current_char == ")"
289
- expect!(",")
290
- skip_ws
291
- end
292
- expect!(")")
293
- pairs
294
- end
295
-
296
- def parse_array
297
- expect!("[")
298
- length = parse_integer_literal
299
- expect!("]")
300
- skip_ws
301
-
302
- header = parse_header if current_char == "{"
303
-
304
- expect!("=")
305
- return [] if length.zero?
306
-
307
- header ? parse_table_rows(length, header) : parse_array_elements(length)
308
- end
309
-
310
- def parse_header
311
- expect!("{")
312
- fields = []
313
- loop do
314
- fields << parse_key_name
315
- break if current_char == "}"
316
- expect!(",")
317
- end
318
- expect!("}")
319
- fields
320
- end
321
-
322
- def parse_table_rows(length, header)
323
- rows = []
324
- length.times do |row_index|
325
- row = {}
326
- header.each_with_index do |field, column_index|
327
- allow_boundary = row_index == length - 1 && column_index == header.length - 1
328
- row[field] = parse_scalar(allow_key_boundary: allow_boundary)
329
- expect!(",") if column_index < header.length - 1
330
- end
331
- rows << symbolize_keys(row)
332
- expect!(";") if row_index < length - 1
333
- end
334
- rows
335
- end
336
-
337
- def parse_array_elements(length)
338
- values = []
339
- length.times do |index|
340
- allow_boundary = index == length - 1
341
- values << parse_value(allow_key_boundary: allow_boundary)
342
- expect!(",") if index < length - 1
343
- end
344
- values
345
- end
346
-
347
- def parse_value(allow_key_boundary: false)
348
- skip_ws
349
- char = current_char
350
- raise ParseError, "Unexpected end of input" if char.nil?
351
-
352
- case char
353
- when "("
354
- parse_object
355
- when "["
356
- parse_array
357
- when '"'
358
- parse_string
359
- else
360
- parse_scalar(allow_key_boundary: allow_key_boundary)
361
- end
362
- end
363
-
364
- def parse_scalar(terminators: TERMINATORS, allow_key_boundary: false)
365
- skip_ws
366
- return parse_string if current_char == '"'
367
-
368
- start = @index
369
- limit_index = allow_key_boundary ? next_key_index(@index) : nil
370
- exit_reason = nil
371
-
372
- while !eof?
373
- if limit_index && @index >= limit_index
374
- exit_reason = :boundary
375
- break
376
- end
377
-
378
- char = current_char
379
-
380
- if char.nil?
381
- exit_reason = :eof
382
- break
383
- elsif terminators.include?(char)
384
- exit_reason = :terminator
385
- break
386
- elsif whitespace?(char)
387
- exit_reason = :whitespace
388
- break
389
- elsif "()[]{}".include?(char)
390
- exit_reason = :structure
391
- break
392
- end
393
-
394
- @index += 1
395
- end
396
-
397
- token = if exit_reason == :boundary && limit_index
398
- @source[start...limit_index]
399
- else
400
- @source[start...@index]
401
- end
402
-
403
- raise ParseError, "Empty value" if token.nil? || token.empty?
404
-
405
- convert_scalar(token)
406
- end
407
-
408
- def convert_scalar(token)
409
- case token
410
- when "true" then true
411
- when "false" then false
412
- when "null" then nil
413
- else
414
- if integer?(token)
415
- token.to_i
416
- elsif float?(token)
417
- token.to_f
418
- else
419
- token
420
- end
421
- end
422
- end
423
-
424
- def parse_string
425
- expect!("\"")
426
- buffer = +""
427
- while !eof?
428
- char = current_char
429
- raise ParseError, "Unterminated string" if char.nil?
430
-
431
- if char == '\\'
432
- @index += 1
433
- escaped = current_char
434
- raise ParseError, "Invalid escape sequence" if escaped.nil?
435
- buffer << case escaped
436
- when 'n' then "\n"
437
- when 'r' then "\r"
438
- when 't' then "\t"
439
- when '"', '\\' then escaped
440
- else
441
- raise ParseError, "Unsupported escape sequence"
442
- end
443
- elsif char == '"'
444
- break
445
- else
446
- buffer << char
447
- end
448
- @index += 1
449
- end
450
- expect!("\"")
451
- buffer
452
- end
453
-
454
- def parse_key_name
455
- skip_ws
456
- start = @index
457
- while !eof? && safe_key_char?(current_char)
458
- @index += 1
459
- end
460
- token = @source[start...@index]
461
- raise ParseError, "Invalid key" if token.nil? || token.empty?
462
- symbolize_names ? token.to_sym : token
463
- end
464
-
465
- def parse_integer_literal
466
- start = @index
467
- while !eof? && current_char =~ /\d/
468
- @index += 1
469
- end
470
- token = @source[start...@index]
471
- raise ParseError, "Expected digits" if token.nil? || token.empty?
472
- Integer(token, 10)
473
- rescue ArgumentError
474
- raise ParseError, "Invalid length literal"
475
- end
476
-
477
- def assign_pair(hash, key, value)
478
- hash[key] = value
479
- end
480
-
481
- def symbolize_keys(row)
482
- symbolize_names ? row.transform_keys(&:to_sym) : row
483
- end
484
-
485
- def expect!(char)
486
- skip_ws
487
- actual = current_char
488
- raise ParseError, "Expected #{char.inspect}, got #{actual.inspect}" unless actual == char
489
- @index += 1
490
- end
491
-
492
- def skip_ws
493
- @index += 1 while !eof? && whitespace?(current_char)
494
- end
495
-
496
- def whitespace?(char)
497
- char == " " || char == "\t" || char == "\n" || char == "\r"
498
- end
499
-
500
- def eof?
501
- @index >= @source.length
502
- end
503
-
504
- def current_char
505
- @source[@index]
506
- end
507
-
508
- def advance
509
- @index += 1
510
- end
511
-
512
- def key_ahead?(offset)
513
- idx = offset
514
- idx += 1 while idx < @source.length && whitespace?(@source[idx])
515
- start = idx
516
- while idx < @source.length && safe_key_char?(@source[idx])
517
- idx += 1
518
- end
519
- return false if idx == start
520
- next_char = @source[idx]
521
- ["(", "[", "="].include?(next_char)
522
- end
523
-
524
- def safe_key_char?(char)
525
- !char.nil? && char.match?(/[0-9A-Za-z_.:-]/)
526
- end
527
-
528
- def integer?(token)
529
- token.match?(/\A-?(?:0|[1-9]\d*)\z/)
530
- end
531
-
532
- def float?(token)
533
- token.match?(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\z/)
534
- end
535
-
536
- def next_key_index(from_index)
537
- idx = from_index
538
- in_string = false
539
-
540
- while idx < @source.length
541
- char = @source[idx]
542
-
543
- if in_string
544
- if char == '\\'
545
- idx += 2
546
- next
547
- elsif char == '"'
548
- in_string = false
549
- idx += 1
550
- next
551
- else
552
- idx += 1
553
- next
554
- end
555
- else
556
- case char
557
- when '"'
558
- in_string = true
559
- idx += 1
560
- next
561
- else
562
- if safe_key_char?(char)
563
- start = idx
564
- idx += 1 while idx < @source.length && safe_key_char?(@source[idx])
565
- next_char = @source[idx]
566
- if start > from_index && ["(", "[", "="].include?(next_char)
567
- return start
568
- end
569
- idx = start + 1
570
- next
571
- end
572
- idx += 1
573
- end
574
- end
575
- end
576
-
577
- nil
578
- end
579
- end
580
25
  end
581
26
 
data/sig/cton.rbs CHANGED
@@ -4,8 +4,80 @@ module Cton
4
4
  class EncodeError < Error; end
5
5
  class ParseError < Error; end
6
6
 
7
- def self.dump: (untyped, ?Hash[Symbol, untyped]) -> String
8
- def self.generate: (untyped, ?Hash[Symbol, untyped]) -> String
9
- def self.load: (String, ?symbolize_names: bool) -> untyped
10
- def self.parse: (String, ?symbolize_names: bool) -> untyped
7
+ def self.dump: (untyped payload, ?Hash[Symbol, untyped] options) -> String
8
+ def self.generate: (untyped payload, ?Hash[Symbol, untyped] options) -> String
9
+ def self.load: (String cton_string, ?symbolize_names: bool) -> untyped
10
+ def self.parse: (String cton_string, ?symbolize_names: bool) -> untyped
11
+
12
+ class Encoder
13
+ SAFE_TOKEN: Regexp
14
+ NUMERIC_TOKEN: Regexp
15
+ RESERVED_LITERALS: Array[String]
16
+ FLOAT_DECIMAL_PRECISION: Integer
17
+
18
+ def initialize: (?separator: String) -> void
19
+ def encode: (untyped payload) -> String
20
+
21
+ private
22
+ attr_reader separator: String
23
+ attr_reader io: StringIO
24
+
25
+ def encode_root: (untyped value) -> void
26
+ def encode_top_level_pair: (String | Symbol key, untyped value) -> void
27
+ def encode_value: (untyped value, context: Symbol) -> void
28
+ def encode_object: (Hash[untyped, untyped] hash) -> void
29
+ def encode_array: (Array[untyped] list) -> void
30
+ def encode_table: (Array[Hash[untyped, untyped]] rows) -> void
31
+ def encode_scalar_list: (Array[untyped] list) -> void
32
+ def encode_mixed_list: (Array[untyped] list) -> void
33
+ def encode_scalar: (untyped value) -> void
34
+ def encode_string: (String value) -> void
35
+ def format_number: (Numeric value) -> String
36
+ def normalize_decimal_string: (String string) -> String
37
+ def zero_string?: (String string) -> bool
38
+ def float_decimal_string: (Numeric value) -> String
39
+ def format_key: (String | Symbol key) -> String
40
+ def string_needs_quotes?: (String value) -> bool
41
+ def numeric_like?: (String value) -> bool
42
+ def quote_string: (String value) -> String
43
+ def escape_string: (String value) -> String
44
+ def scalar?: (untyped value) -> bool
45
+ def table_candidate?: (Array[untyped] rows) -> bool
46
+ end
47
+
48
+ class Decoder
49
+ TERMINATORS: Array[String]
50
+
51
+ def initialize: (?symbolize_names: bool) -> void
52
+ def decode: (String cton) -> untyped
53
+
54
+ private
55
+ attr_reader symbolize_names: bool
56
+ attr_reader scanner: StringScanner
57
+
58
+ def parse_document: -> Hash[untyped, untyped]
59
+ def parse_value_for_key: -> untyped
60
+ def parse_object: -> Hash[untyped, untyped]
61
+ def parse_array: -> Array[untyped]
62
+ def parse_header: -> Array[String | Symbol]
63
+ def parse_table_rows: (Integer length, Array[String | Symbol] header) -> Array[Hash[untyped, untyped]]
64
+ def parse_array_elements: (Integer length) -> Array[untyped]
65
+ def parse_value: (?allow_key_boundary: bool) -> untyped
66
+ def parse_scalar: (?allow_key_boundary: bool) -> untyped
67
+ def scan_until_terminator: -> String?
68
+ def scan_until_boundary_or_terminator: -> String?
69
+ def find_key_boundary: (Integer from_index) -> Integer?
70
+ def convert_scalar: (String token) -> untyped
71
+ def parse_string: -> String
72
+ def parse_key_name: -> (String | Symbol)
73
+ def parse_integer_literal: -> Integer
74
+ def symbolize_keys: (Hash[untyped, untyped] row) -> Hash[untyped, untyped]
75
+ def expect!: (String char) -> void
76
+ def skip_ws: -> void
77
+ def whitespace?: (String char) -> bool
78
+ def key_ahead?: -> bool
79
+ def safe_key_char?: (String? char) -> bool
80
+ def integer?: (String token) -> bool
81
+ def float?: (String token) -> bool
82
+ end
11
83
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cton
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Davide Santangelo
@@ -26,6 +26,8 @@ files:
26
26
  - README.md
27
27
  - Rakefile
28
28
  - lib/cton.rb
29
+ - lib/cton/decoder.rb
30
+ - lib/cton/encoder.rb
29
31
  - lib/cton/version.rb
30
32
  - sig/cton.rbs
31
33
  homepage: https://github.com/davidesantangelo/cton