rbtoon 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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon
4
+ class Handler # :nodoc: all
5
+ def initialize
6
+ @stack = [Nodes::Root.new]
7
+ end
8
+
9
+ def output
10
+ @stack.first
11
+ end
12
+
13
+ def push_object(key)
14
+ object = Nodes::Object.new(current, key.position)
15
+ push_value(object)
16
+
17
+ @stack << object
18
+ push_value(key)
19
+ end
20
+
21
+ def push_array(l_bracket_token, size)
22
+ array = Nodes::Array.new(current, l_bracket_token.position, size)
23
+ push_value(array)
24
+
25
+ @stack << array
26
+ end
27
+
28
+ def push_value(value)
29
+ current.push_value(value)
30
+ end
31
+
32
+ def push_values(values)
33
+ values.each { |value| push_value(value) }
34
+ end
35
+
36
+ def push_tabular_fields(fields)
37
+ current.push_tabular_fields(fields)
38
+ end
39
+
40
+ def push_tabular_row(values)
41
+ row = Nodes::TabularRow.new(values, values.first.position)
42
+ push_value(row)
43
+ end
44
+
45
+ def push_blank(token)
46
+ return unless token
47
+
48
+ blank = Nodes::Blank.new(token)
49
+ push_value(blank)
50
+ end
51
+
52
+ def push_empty_object(position)
53
+ object = Nodes::EmptyObject.new(position)
54
+ push_value(object)
55
+ end
56
+
57
+ def pop
58
+ @stack.pop
59
+ end
60
+
61
+ def quoted_string(token)
62
+ Nodes::QuotedString.new(token)
63
+ end
64
+
65
+ def unquoted_string(token)
66
+ Nodes::UnquotedString.new(token)
67
+ end
68
+
69
+ def empty_string(position)
70
+ Nodes::EmptyString.new(position)
71
+ end
72
+
73
+ def boolean(token)
74
+ Nodes::Boolean.new(token)
75
+ end
76
+
77
+ def null(token)
78
+ Nodes::Null.new(token)
79
+ end
80
+
81
+ def number(token)
82
+ Nodes::Number.new(token)
83
+ end
84
+
85
+ private
86
+
87
+ def current
88
+ @stack.last
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon # :nodoc: all
4
+ module Nodes
5
+ class Array < StructureBase
6
+ def initialize(parent, position, size)
7
+ super(parent, position)
8
+ @size = size
9
+ end
10
+
11
+ def push_tabular_fields(fields)
12
+ @fields = fields
13
+ end
14
+
15
+ def validate(strict: true)
16
+ if @fields
17
+ validate_tabular_array(strict)
18
+ else
19
+ validate_array(strict)
20
+ end
21
+ end
22
+
23
+ def to_ruby(symbolize_names: false, **optargs)
24
+ values = non_blank_values
25
+ result =
26
+ if tabular?
27
+ to_ruby_tabular(values, symbolize_names, **optargs)
28
+ else
29
+ to_ruby_list(values, symbolize_names, **optargs)
30
+ end
31
+ result || []
32
+ end
33
+
34
+ def kind
35
+ :array
36
+ end
37
+
38
+ private
39
+
40
+ def tabular?
41
+ !@fields.nil?
42
+ end
43
+
44
+ def validate_tabular_array(strict)
45
+ check_blank(strict, 'tabular rows')
46
+ validate_array_size('tabular rows')
47
+ validate_tabular_row_size
48
+ @valus&.flatten&.each { |value| value.validate(strict:) }
49
+ end
50
+
51
+ def validate_array(strict)
52
+ check_blank(strict, 'array')
53
+ validate_array_size('array items')
54
+ @values&.each { |value| value.validate(strict:) }
55
+ end
56
+
57
+ def validate_array_size(kind)
58
+ actual = non_blank_values&.size || 0
59
+ expected = @size.to_ruby
60
+ return if actual == expected
61
+
62
+ raise_parse_error "expected #{expected} #{kind}, but got #{actual}", position
63
+ end
64
+
65
+ def validate_tabular_row_size
66
+ expected = @fields.size
67
+ non_blank_values.each do |row|
68
+ actual = row.size
69
+ next if actual == expected
70
+
71
+ position = row.position
72
+ raise_parse_error "expected #{expected} tabular row items, but got #{actual}", position
73
+ end
74
+ end
75
+
76
+ def to_ruby_tabular(rows, symbolize_names, **optargs)
77
+ return unless rows
78
+
79
+ fields = to_ruby_list(@fields, symbolize_names, **optargs)
80
+ fields.map!(&:to_sym) if symbolize_names
81
+
82
+ rows.map do |row|
83
+ fields
84
+ .zip(row.to_ruby(symbolize_names:, **optargs))
85
+ .to_h
86
+ end
87
+ end
88
+
89
+ def to_ruby_list(values, symbolize_names, **optargs)
90
+ values&.map { |value| value.to_ruby(symbolize_names:, **optargs) }
91
+ end
92
+ end
93
+
94
+ class TabularRow < Base
95
+ def initialize(values, position)
96
+ super(position)
97
+ @values = values
98
+ end
99
+
100
+ def size
101
+ @values.size
102
+ end
103
+
104
+ def to_ruby(**optargs)
105
+ @values.map { |value| value.to_ruby(**optargs) }
106
+ end
107
+
108
+ def kind
109
+ :tabular_row
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon # :nodoc: all
4
+ module Nodes
5
+ class Base
6
+ include RaiseParseError
7
+
8
+ def initialize(position)
9
+ @position = position
10
+ end
11
+
12
+ attr_reader :position
13
+
14
+ [
15
+ :array, :tabular_row, :blank, :object, :empty_object,
16
+ :root, :quoted_string, :unquoted_string, :empty_string,
17
+ :boolean, :null, :number
18
+ ].each do |kind|
19
+ class_eval(<<~M, __FILE__, __LINE__ + 1)
20
+ # def array?
21
+ # kind == :array
22
+ # end
23
+ def #{kind}?
24
+ kind == :#{kind}
25
+ end
26
+ M
27
+ end
28
+
29
+ def validate(strict: true)
30
+ end
31
+ end
32
+
33
+ class StructureBase < Base
34
+ def initialize(parent, position)
35
+ super(position)
36
+ @parent = parent
37
+ end
38
+
39
+ attr_reader :parent
40
+
41
+ def push_value(value)
42
+ (@values ||= []) << value
43
+ end
44
+
45
+ private
46
+
47
+ def non_blank_values
48
+ @values&.reject(&:blank?)
49
+ end
50
+
51
+ def check_blank(strict, kind)
52
+ return unless strict && @values && inside_array?
53
+
54
+ blank =
55
+ if parent.avove_array_edge?(self)
56
+ @values.index(&:blank?)
57
+ else
58
+ @values[..-2].index(&:blank?)
59
+ end
60
+ return unless blank
61
+
62
+ position = @values[blank].position
63
+ raise_parse_error "blank lines inside #{kind} are not allowed", position
64
+ end
65
+
66
+ protected
67
+
68
+ def inside_array?
69
+ array? || parent&.inside_array? || false
70
+ end
71
+
72
+ def avove_array_edge?(object)
73
+ return false unless inside_array?
74
+ return true unless @values.last.equal?(object)
75
+
76
+ parent.avove_array_edge?(self)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon # :nodoc: all
4
+ module Nodes
5
+ class Blank < Base
6
+ def initialize(token)
7
+ super(token.position)
8
+ end
9
+
10
+ def kind
11
+ :blank
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon # :nodoc: all
4
+ module Nodes
5
+ class Object < StructureBase
6
+ def validate(strict: true)
7
+ check_blank(strict, 'array')
8
+ each_key_value do |key, value|
9
+ key.validate(strict:)
10
+ value.validate(strict:)
11
+ end
12
+ end
13
+
14
+ def to_ruby(symbolize_names: false, strict: true, path_expansion: false)
15
+ each_key_value.with_object({}) do |(key, value), result|
16
+ build_result(result, key, value, symbolize_names, strict, path_expansion)
17
+ end
18
+ end
19
+
20
+ def kind
21
+ :object
22
+ end
23
+
24
+ private
25
+
26
+ def each_key_value(&)
27
+ if block_given?
28
+ non_blank_values.each_slice(2, &)
29
+ else
30
+ non_blank_values.each_slice(2)
31
+ end
32
+ end
33
+
34
+ def build_result(
35
+ result, key, value,
36
+ symbolize_names, strict, path_expansion
37
+ )
38
+ k = key.to_ruby(symbolize_names:, strict:, path_expansion:)
39
+ v = value.to_ruby(symbolize_names:, strict:, path_expansion:)
40
+
41
+ paths = split_path(key, k, symbolize_names, path_expansion)
42
+ insert_value(result, paths, v, strict, key.position)
43
+ end
44
+
45
+ def split_path(key_node, key, symbolize_names, path_expansion)
46
+ paths =
47
+ if path_expansion && expandable_key?(key_node, key)
48
+ key.split('.')
49
+ else
50
+ [key]
51
+ end
52
+
53
+ paths.map!(&:to_sym) if symbolize_names
54
+ paths
55
+ end
56
+
57
+ def expandable_key?(key_node, key)
58
+ key_node.unquoted_string? &&
59
+ /\A[_a-z][_a-z0-9]*(?:\.[_a-z][_a-z0-9]*)*\Z/i.match?(key)
60
+ end
61
+
62
+ def insert_value(result, paths, value, strict, position)
63
+ check_conflict(result, paths, value, strict, position)
64
+
65
+ path = paths.first
66
+ if paths.size > 1
67
+ result[path] = {} unless result[path].is_a?(Hash)
68
+ insert_value(result[path], paths[1..], value, strict, position)
69
+ elsif [result[path], value] in [Hash, Hash]
70
+ result[path].merge!(value)
71
+ else
72
+ result[path] = value
73
+ end
74
+ end
75
+
76
+ def check_conflict(result, paths, value, strict, position)
77
+ return unless conflict?(result, paths, value, strict)
78
+
79
+ raise_parse_error "key conflict at \"#{paths.first}\"", position
80
+ end
81
+
82
+ def conflict?(result, paths, value, strict)
83
+ path = paths.first
84
+ return false unless strict && result.key?(path)
85
+
86
+ if paths.size > 1
87
+ !result[path].is_a?(Hash)
88
+ elsif [result[path], value] in [Hash, Hash]
89
+ !result[path].keys.intersect?(value.keys)
90
+ else
91
+ true
92
+ end
93
+ end
94
+ end
95
+
96
+ class EmptyObject < Base
97
+ def to_ruby(**_optargs)
98
+ {}
99
+ end
100
+
101
+ def kind
102
+ :empty_object
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon # :nodoc: all
4
+ module Nodes
5
+ class Root < Array
6
+ def initialize
7
+ super(nil, nil, nil)
8
+ end
9
+
10
+ def validate(strict: true)
11
+ @values&.each_with_index do |value, i|
12
+ i.positive? &&
13
+ (raise_parse_error 'two or more values at root depth', value.position)
14
+ value.validate(strict:)
15
+ end
16
+ end
17
+
18
+ def to_ruby(**optargs)
19
+ return {} unless @values
20
+
21
+ @values.first.to_ruby(**optargs)
22
+ end
23
+
24
+ def kind
25
+ :root
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon # :nodoc: all
4
+ module Nodes
5
+ class Scalar < Base
6
+ def initialize(token)
7
+ super(token.position)
8
+ @token = token
9
+ end
10
+
11
+ attr_reader :token
12
+ end
13
+
14
+ class QuotedString < Scalar
15
+ def to_ruby(**_optargs)
16
+ token.text[1..-2]
17
+ end
18
+
19
+ def kind
20
+ :quoted_string
21
+ end
22
+ end
23
+
24
+ class UnquotedString < Scalar
25
+ def to_ruby(**_optargs)
26
+ token.text.strip
27
+ end
28
+
29
+ def kind
30
+ :unquoted_string
31
+ end
32
+ end
33
+
34
+ class EmptyString < Base
35
+ def to_ruby(**_optargs)
36
+ ''
37
+ end
38
+
39
+ def kind
40
+ :empty_string
41
+ end
42
+ end
43
+
44
+ class Boolean < Scalar
45
+ def to_ruby(**_optargs)
46
+ token.text.strip == 'true'
47
+ end
48
+
49
+ def kind
50
+ :boolean
51
+ end
52
+ end
53
+
54
+ class Null < Scalar
55
+ def to_ruby(**_optargs)
56
+ nil
57
+ end
58
+
59
+ def kind
60
+ :null
61
+ end
62
+ end
63
+
64
+ class Number < Scalar
65
+ def to_ruby(**_optargs)
66
+ text = token.text.strip
67
+ if text.match?(/[.e]/i)
68
+ value_f = text.to_f
69
+ value_i = value_f.to_i
70
+ value_f == value_i ? value_i : value_f
71
+ else
72
+ text.to_i
73
+ end
74
+ end
75
+
76
+ def kind
77
+ :number
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon
4
+ ##
5
+ # The exception class is raised when the given Toon includes errors.
6
+ #
7
+ # Fields:
8
+ # +error_message+::
9
+ # Message string of the detected error.
10
+ # +position+::
11
+ # Position information where the error is detected.
12
+ class ParseError < StandardError
13
+ def initialize(error_message, position)
14
+ super(error_message)
15
+ @error_message = error_message
16
+ @position = position
17
+ end
18
+
19
+ ##
20
+ # Read accessor to +error_message+ field.
21
+ attr_reader :error_message
22
+
23
+ ##
24
+ # Read accessor to +position+ field.
25
+ attr_reader :position
26
+
27
+ ##
28
+ # Return the +error_message+ string.
29
+ # The +position+ information is also included if provided.
30
+ def to_s
31
+ (position && "#{super} -- #{position}") || super
32
+ end
33
+ end
34
+
35
+ module RaiseParseError # :nodoc:
36
+ private
37
+
38
+ def raise_parse_error(message, position = nil)
39
+ raise ParseError.new(message, position)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RbToon
4
+ class Parser < GeneratedParser # :nodoc:
5
+ include RaiseParseError
6
+
7
+ def initialize(scanner, handler, debug: false)
8
+ @scanner = scanner
9
+ @handler = handler
10
+ @yydebug = debug
11
+ super()
12
+ end
13
+
14
+ def parse
15
+ do_parse
16
+ handler.output
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :scanner
22
+ attr_reader :handler
23
+
24
+ def next_token
25
+ scanner.next_token
26
+ end
27
+
28
+ def on_error(_token_id, value, _value_stack)
29
+ message = "syntax error on value '#{value.text}' (#{value.kind})"
30
+ raise_parse_error message, value.position
31
+ end
32
+
33
+ def to_list(val)
34
+ [val[0], *val[1]&.map { |_, value| value }]
35
+ end
36
+ end
37
+ end