jmespath-ruby 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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +18 -0
  3. data/CONTRIBUTING.md +57 -0
  4. data/LICENSE +21 -0
  5. data/LICENSES/python-MIT.txt +21 -0
  6. data/LICENSES/ruby-Apache-2.0.txt +174 -0
  7. data/README.md +122 -0
  8. data/SECURITY.md +16 -0
  9. data/VERSIONING.md +12 -0
  10. data/docs/guides/compatibility.md +84 -0
  11. data/docs/guides/compilation.md +63 -0
  12. data/docs/guides/errors.md +30 -0
  13. data/docs/guides/functions.md +65 -0
  14. data/docs/guides/performance.md +237 -0
  15. data/docs/guides/search.md +56 -0
  16. data/exe/jmespath +5 -0
  17. data/exe/jmespath.rb +5 -0
  18. data/lib/jmespath/ast.rb +43 -0
  19. data/lib/jmespath/cache.rb +53 -0
  20. data/lib/jmespath/cli.rb +36 -0
  21. data/lib/jmespath/data_access.rb +66 -0
  22. data/lib/jmespath/errors.rb +53 -0
  23. data/lib/jmespath/evaluation/construction.rb +20 -0
  24. data/lib/jmespath/evaluation/logic.rb +42 -0
  25. data/lib/jmespath/evaluation/projections.rb +43 -0
  26. data/lib/jmespath/evaluation/scalars.rb +49 -0
  27. data/lib/jmespath/evaluation/slices.rb +50 -0
  28. data/lib/jmespath/evaluator.rb +37 -0
  29. data/lib/jmespath/expression.rb +63 -0
  30. data/lib/jmespath/expression_reference.rb +15 -0
  31. data/lib/jmespath/functions/coercions.rb +41 -0
  32. data/lib/jmespath/functions/collections.rb +84 -0
  33. data/lib/jmespath/functions/conversion.rb +32 -0
  34. data/lib/jmespath/functions/function_call.rb +32 -0
  35. data/lib/jmespath/functions/references.rb +62 -0
  36. data/lib/jmespath/functions/scalar.rb +74 -0
  37. data/lib/jmespath/functions/signature.rb +77 -0
  38. data/lib/jmespath/functions.rb +73 -0
  39. data/lib/jmespath/input.rb +19 -0
  40. data/lib/jmespath/lexer.rb +132 -0
  41. data/lib/jmespath/normalizer.rb +53 -0
  42. data/lib/jmespath/options.rb +67 -0
  43. data/lib/jmespath/parser/expressions.rb +70 -0
  44. data/lib/jmespath/parser/projections.rb +91 -0
  45. data/lib/jmespath/parser.rb +119 -0
  46. data/lib/jmespath/plan.rb +153 -0
  47. data/lib/jmespath/token.rb +6 -0
  48. data/lib/jmespath/types.rb +84 -0
  49. data/lib/jmespath/version.rb +7 -0
  50. data/lib/jmespath.rb +100 -0
  51. data/sig/jmespath.rbs +55 -0
  52. metadata +135 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ class Signature
7
+ TYPES = %i[any null boolean number string array object expref array_number array_string].freeze
8
+
9
+ # Identifies the invocation that created a validation failure.
10
+ module FailureOrigin
11
+ attr_reader :origin
12
+
13
+ def initialize(message = nil, origin: nil)
14
+ @origin = origin
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ # An error raised by the library's argument validator.
20
+ class TypeMismatch < Errors::InvalidTypeError
21
+ include FailureOrigin
22
+ end
23
+
24
+ # An error raised by the library's arity validator.
25
+ class ArityMismatch < Errors::InvalidArityError
26
+ include FailureOrigin
27
+ end
28
+
29
+ def initialize(arguments, variadic: false)
30
+ raise ArgumentError, "Invalid function signature" unless valid_definition?(arguments)
31
+ raise ArgumentError, "Variadic signatures require an argument" if variadic && arguments.empty?
32
+
33
+ @arguments = arguments.map { |types| types.dup.freeze }.freeze
34
+ @variadic = variadic
35
+ freeze
36
+ end
37
+
38
+ def validate!(values, name, origin: nil)
39
+ unless valid_arity?(values.length)
40
+ raise ArityMismatch.new("Invalid number of arguments for #{name}", origin: origin)
41
+ end
42
+
43
+ values.each_with_index do |value, index|
44
+ types = @arguments.fetch(index) { @arguments.fetch(-1) }
45
+ next if types.any? { |type| matches?(value, type) }
46
+
47
+ raise TypeMismatch.new("Invalid argument #{index + 1} for #{name}: #{Types.name(value)}", origin: origin)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def valid_definition?(arguments)
54
+ arguments.is_a?(Array) && arguments.all? do |types|
55
+ types.is_a?(Array) && !types.empty? && (types - TYPES).empty?
56
+ end
57
+ end
58
+
59
+ def valid_arity?(count)
60
+ @variadic ? count >= @arguments.length : count == @arguments.length
61
+ end
62
+
63
+ def matches?(value, type)
64
+ return true if type == :any
65
+ return array_matches?(value, :number) if type == :array_number
66
+ return array_matches?(value, :string) if type == :array_string
67
+
68
+ Types.name(value) == type
69
+ end
70
+
71
+ def array_matches?(value, type)
72
+ array = DataAccess.array(value)
73
+ array&.all? { |item| Types.name(item) == type }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # Immutable definitions for functions available to an expression.
5
+ class Functions
6
+ # @api private
7
+ Definition = Data.define(:signature, :implementation)
8
+
9
+ # Create an empty immutable function registry.
10
+ def initialize
11
+ # @type var definitions: Hash[String, Definition]
12
+ definitions = {}
13
+ @definitions = definitions.freeze
14
+ freeze
15
+ end
16
+
17
+ # Return an independent registry with one additional definition.
18
+ # @param name [String] unquoted function identifier
19
+ # @param signature [Array<Array<Symbol>>] accepted types for each argument
20
+ # @param variadic [Boolean] repeat the final argument type
21
+ # @param override [Boolean] explicitly replace a definition
22
+ # @yieldparam arguments [Object] evaluated arguments
23
+ # @yieldreturn [Object] the function result
24
+ # @raise [ArgumentError] the definition is invalid or duplicates an existing name
25
+ # @return [Functions] the extended registry
26
+ def with(name, signature:, variadic: false, override: false, &block)
27
+ raise ArgumentError, "A function block is required" unless block
28
+
29
+ definition = Definition.new(Signature.new(signature, variadic: variadic), block)
30
+ add(name, definition, override: override)
31
+ end
32
+
33
+ # @api private
34
+ def with_builtin(name, signature:, handler:, variadic: false)
35
+ definition = Definition.new(Signature.new(signature, variadic: variadic), handler)
36
+ add(name, definition, override: false)
37
+ end
38
+
39
+ # Evaluate a registered function with checked arguments.
40
+ # @param name [String] function identifier
41
+ # @param arguments [Array<Object>] evaluated values
42
+ # @param disable_visit_errors [Boolean] return nil for validation failures
43
+ # @return [Object] the callback result
44
+ # @raise [Errors::UnknownFunctionError] the name is absent
45
+ # @raise [Errors::InvalidTypeError] argument types are incompatible
46
+ # @raise [Errors::InvalidArityError] the number of arguments is invalid
47
+ def call(name, arguments, disable_visit_errors: false)
48
+ definition = @definitions.fetch(name) { raise Errors::UnknownFunctionError, "Unknown function: #{name}" }
49
+ invocation = disable_visit_errors ? FunctionCall.new : FunctionCall::STRICT
50
+ invocation.call(definition, name, arguments, disable_visit_errors: disable_visit_errors)
51
+ end
52
+
53
+ # @api private
54
+ def definition(name)
55
+ @definitions[name]
56
+ end
57
+
58
+ private
59
+
60
+ def add(name, definition, override:)
61
+ unless name.is_a?(String) && name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
62
+ raise ArgumentError, "Invalid function name"
63
+ end
64
+ raise ArgumentError, "Function #{name} is already registered" if @definitions.key?(name) && !override
65
+
66
+ result = dup
67
+ result.instance_variable_set(:@definitions, @definitions.merge(name.dup.freeze => definition).freeze)
68
+ result.freeze
69
+ end
70
+ end
71
+ end
72
+
73
+ require_relative "functions/signature"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "stringio"
5
+
6
+ module JMESPath
7
+ # @api private
8
+ module Input
9
+ def self.load(data)
10
+ return data if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Struct)
11
+
12
+ case data
13
+ when Pathname then File.open(data, "r", encoding: "UTF-8") { |file| JSON.parse(file.read) }
14
+ when IO, StringIO then JSON.parse(data.read.to_s)
15
+ else data
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module JMESPath
6
+ # @api private
7
+ class Lexer
8
+ SINGLE = {
9
+ "[" => :lbracket, "]" => :rbracket, "{" => :lbrace, "}" => :rbrace,
10
+ "(" => :lparen, ")" => :rparen, "," => :comma, ":" => :colon,
11
+ "." => :dot, "*" => :star, "@" => :current, "&" => :expref,
12
+ "|" => :pipe, "!" => :not, "<" => :lt, ">" => :gt
13
+ }.freeze
14
+ DOUBLE = {
15
+ "[]" => :flatten, "[?" => :filter, "&&" => :and, "||" => :or,
16
+ "==" => :eq, "!=" => :ne, "<=" => :lte, ">=" => :gte
17
+ }.freeze
18
+ WHITESPACE = [9, 10, 13, 32].freeze
19
+
20
+ attr_reader :source
21
+
22
+ def initialize(source, max_depth: 128)
23
+ @source = source.encode(Encoding::UTF_8).freeze
24
+ raise Encoding::InvalidByteSequenceError unless @source.valid_encoding?
25
+
26
+ @index = 0
27
+ @max_depth = max_depth
28
+ rescue EncodingError
29
+ raise Errors::SyntaxError, "Expression has invalid encoding"
30
+ end
31
+
32
+ def tokens
33
+ # @type var result: Array[Token?]
34
+ result = []
35
+ result << next_token while @index < source.bytesize
36
+ result << Token.new(:eof, nil, @index, @index)
37
+ result.compact.freeze
38
+ end
39
+
40
+ private
41
+
42
+ def next_token
43
+ start = @index
44
+ byte = source.getbyte(@index)
45
+ return skip_whitespace if WHITESPACE.include?(byte)
46
+ return quoted_token(start, byte) if [34, 39, 96].include?(byte)
47
+ return word_token(start) if identifier_start?(byte)
48
+ return number_token(start) if byte == 45 || digit?(byte)
49
+
50
+ operator_token(start)
51
+ end
52
+
53
+ def skip_whitespace
54
+ @index += 1 while WHITESPACE.include?(source.getbyte(@index))
55
+ nil
56
+ end
57
+
58
+ def word_token(start)
59
+ @index += 1 while identifier_part?(source.getbyte(@index))
60
+ Token.new(:identifier, source.byteslice(start...@index), start, @index)
61
+ end
62
+
63
+ def number_token(start)
64
+ @index += 1 if source.getbyte(@index) == 45
65
+ syntax!("Expected a digit", start) unless digit?(source.getbyte(@index))
66
+ @index += 1 while digit?(source.getbyte(@index))
67
+ Token.new(:number, source.byteslice(start...@index).to_i, start, @index)
68
+ end
69
+
70
+ def operator_token(start)
71
+ pair = source.byteslice(start, 2)
72
+ type = DOUBLE[pair]
73
+ length = type ? 2 : 1
74
+ type ||= SINGLE[source.byteslice(start, 1)]
75
+ syntax!("Unexpected character", start) unless type
76
+ @index += length
77
+ Token.new(type, nil, start, @index)
78
+ end
79
+
80
+ def quoted_token(start, quote)
81
+ @index += 1
82
+ content_start = @index
83
+ scan_quote(quote, start)
84
+ content = source.byteslice(content_start...@index).to_s
85
+ @index += 1
86
+ value = decode(content, quote)
87
+ Token.new(quote == 34 ? :identifier : :literal, value, start, @index)
88
+ rescue JSON::ParserError => e
89
+ syntax!("Invalid quoted value: #{e.message}", start)
90
+ end
91
+
92
+ def scan_quote(quote, start)
93
+ while @index < source.bytesize
94
+ return if source.getbyte(@index) == quote
95
+
96
+ @index += source.getbyte(@index) == 92 ? 2 : 1
97
+ end
98
+ syntax!("Unterminated quoted value", start)
99
+ end
100
+
101
+ def decode(content, quote)
102
+ return JSON.parse("\"#{content}\"") if quote == 34
103
+ return content.gsub("\\'", "'") if quote == 39
104
+
105
+ literal(content.gsub("\\#{96.chr}", 96.chr))
106
+ end
107
+
108
+ def literal(content)
109
+ JSON.parse(content, max_nesting: @max_depth)
110
+ rescue JSON::NestingError
111
+ raise
112
+ rescue JSON::ParserError
113
+ JSON.parse("\"#{content.lstrip}\"", max_nesting: @max_depth)
114
+ end
115
+
116
+ def identifier_start?(byte)
117
+ byte && (byte.between?(65, 90) || byte.between?(97, 122) || byte == 95)
118
+ end
119
+
120
+ def identifier_part?(byte)
121
+ identifier_start?(byte) || digit?(byte)
122
+ end
123
+
124
+ def digit?(byte)
125
+ byte&.between?(48, 57)
126
+ end
127
+
128
+ def syntax!(message, position)
129
+ raise Errors::SyntaxError.new(message, expression: source, position: position)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Normalizer
6
+ def initialize
7
+ @pending = []
8
+ @active = {}
9
+ end
10
+
11
+ def call(value)
12
+ # @type var result: Array[value]
13
+ result = [nil]
14
+ enqueue(value) { |entry| result[0] = entry }
15
+ while (operation = @pending.pop)
16
+ operation.call
17
+ end
18
+ result.first
19
+ end
20
+
21
+ private
22
+
23
+ def enqueue(value, &assign)
24
+ @pending << -> { visit(value, &assign) }
25
+ end
26
+
27
+ def visit(value, &assign)
28
+ collection = DataAccess.array(value) || DataAccess.object(value)
29
+ return assign.call(DataAccess.string(value) || value) unless collection
30
+
31
+ id = value.object_id
32
+ raise Errors::InvalidValueError, "Cyclic values cannot be compared or serialized" if @active[id]
33
+
34
+ @active[id] = true
35
+ @pending << -> { @active.delete(id) }
36
+ collection.is_a?(Array) ? array(collection, &assign) : object(collection, &assign)
37
+ end
38
+
39
+ def array(values)
40
+ # @type var result: Array[value]
41
+ result = Array.new(values.length)
42
+ yield result
43
+ (values.length - 1).downto(0) { |index| enqueue(values[index]) { |entry| result[index] = entry } }
44
+ end
45
+
46
+ def object(values)
47
+ # @type var result: Hash[value, value]
48
+ result = values.transform_values { nil }
49
+ yield result
50
+ values.to_a.reverse_each { |key, value| enqueue(value) { |entry| result[key] = entry } }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Options
6
+ # rubocop:disable-next Style/RedundantParentheses -- Type the empty hash before freezing it.
7
+ EMPTY = (
8
+ {} # : Hash[Symbol, value]
9
+ ).freeze
10
+ KEYS = %i[cache_expressions disable_visit_errors functions max_depth].freeze
11
+ attr_reader :cache_expressions, :disable_visit_errors, :functions, :max_depth
12
+
13
+ def initialize(positional = {}, keywords = {}, default_depth: 128, defaults: nil)
14
+ values = merge(positional, keywords)
15
+ @cache_expressions = boolean(values.fetch(:cache_expressions, true))
16
+ @disable_visit_errors = boolean(values.fetch(:disable_visit_errors) do
17
+ default_value(defaults, :disable_visit_errors)
18
+ end)
19
+ @max_depth = depth(values.fetch(:max_depth, default_depth))
20
+ @functions = registry(values.fetch(:functions) { default_value(defaults, :functions) })
21
+ freeze
22
+ end
23
+
24
+ private
25
+
26
+ def default_value(defaults, name)
27
+ return defaults.public_send(name) if defaults
28
+
29
+ name == :functions ? Functions.default : false
30
+ end
31
+
32
+ def merge(positional, keywords)
33
+ raise ArgumentError, "Options must be a Hash" unless positional.is_a?(Hash)
34
+
35
+ keywords.each_key do |key|
36
+ raise ArgumentError, "Conflicting option: #{key}" if positional.key?(key) && positional[key] != keywords[key]
37
+ end
38
+ values = positional.merge(keywords)
39
+ raise ArgumentError, "Unknown search options" unless (values.keys - KEYS).empty?
40
+
41
+ values
42
+ end
43
+
44
+ def boolean(value)
45
+ case value
46
+ when TrueClass, FalseClass then value
47
+ else raise ArgumentError, "Expected a boolean option"
48
+ end
49
+ end
50
+
51
+ def depth(value)
52
+ unless value.is_a?(Integer) && (1..128).cover?(value)
53
+ raise ArgumentError, "max_depth must be an integer from 1 through 128"
54
+ end
55
+
56
+ value
57
+ end
58
+
59
+ def registry(value)
60
+ raise ArgumentError, "functions must be a Functions registry" unless value.is_a?(Functions)
61
+
62
+ value
63
+ end
64
+ end
65
+
66
+ Options::DEFAULT = Options.new
67
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Parser
6
+ private
7
+
8
+ def prefix_identifier(token)
9
+ node(:field, token.value, [], start: token.start)
10
+ end
11
+
12
+ def prefix_literal(token)
13
+ node(:literal, token.value, [], start: token.start)
14
+ end
15
+
16
+ def prefix_current(token)
17
+ node(:current, nil, [], start: token.start)
18
+ end
19
+
20
+ def prefix_group(token)
21
+ inner = expression
22
+ expect(:rparen)
23
+ node(:group, nil, [inner], start: token.start)
24
+ end
25
+
26
+ def prefix_not(token)
27
+ node(:not, nil, [expression(45)], start: token.start)
28
+ end
29
+
30
+ def prefix_reference(token)
31
+ node(:expression_reference, nil, [expression], start: token.start)
32
+ end
33
+
34
+ def prefix_hash(token)
35
+ pairs = separated(:rbrace) do
36
+ key = expect(:identifier)
37
+ expect(:colon)
38
+ node(:pair, key.value, [expression], start: key.start)
39
+ end
40
+ node(:multi_select_hash, nil, pairs, start: token.start)
41
+ end
42
+
43
+ def multi_select_list(token)
44
+ items = separated(:rbracket) { expression }
45
+ node(:multi_select_list, nil, items, start: token.start)
46
+ end
47
+
48
+ def infix_function(left, token)
49
+ unless left.kind == :field && @source.getbyte(left.start) != 34
50
+ syntax!("Function name must be an unquoted identifier", token)
51
+ end
52
+ arguments = separated(:rparen, empty: true) { expression }
53
+ node(:function, left.value, arguments, start: left.start)
54
+ end
55
+
56
+ def infix_dot(left, _token)
57
+ return node(:value_projection, nil, [left, projection_rhs(20)]) if peek.type == :star && advance
58
+
59
+ sequence(:subexpression, left, dot_rhs(40))
60
+ end
61
+
62
+ def dot_rhs(power)
63
+ return multi_select_list(advance) if peek.type == :lbracket
64
+
65
+ syntax!("Expected a field, function, or multiselect after dot") unless %i[identifier lbrace
66
+ star].include?(peek.type)
67
+ expression(power)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Parser
6
+ private
7
+
8
+ def prefix_bracket(token)
9
+ if %i[number colon].include?(peek.type) || (peek.type == :star && @tokens.fetch(@index + 1).type == :rbracket)
10
+ return bracket_body(node(:current))
11
+ end
12
+
13
+ multi_select_list(token)
14
+ end
15
+
16
+ def infix_bracket(left, _token)
17
+ bracket_body(left)
18
+ end
19
+
20
+ def bracket_body(left)
21
+ if peek.type == :star
22
+ advance
23
+ expect(:rbracket)
24
+ return node(:projection, nil, [left, projection_rhs(20)])
25
+ end
26
+ index_or_slice(left)
27
+ end
28
+
29
+ def index_or_slice(left)
30
+ start = integer if peek.type == :number
31
+ return slice(left, start) if peek.type == :colon
32
+
33
+ syntax!("Expected an array index") unless start
34
+ expect(:rbracket)
35
+ node(:index, start, [left])
36
+ end
37
+
38
+ def slice(left, start)
39
+ advance
40
+ stop = integer if peek.type == :number
41
+ step = slice_step
42
+ expect(:rbracket)
43
+ raise Errors::InvalidValueError, "Slice step cannot be zero" if step&.zero?
44
+
45
+ base = node(:slice, [start, stop, step], [left])
46
+ node(:projection, nil, [base, projection_rhs(20)])
47
+ end
48
+
49
+ def slice_step
50
+ return nil unless peek.type == :colon
51
+
52
+ advance
53
+ integer if peek.type == :number
54
+ end
55
+
56
+ def integer
57
+ advance.value.to_s.to_i
58
+ end
59
+
60
+ def prefix_star(_token)
61
+ node(:value_projection, nil, [node(:current), projection_rhs(20)])
62
+ end
63
+
64
+ def prefix_flatten(token)
65
+ infix_flatten(node(:current), token)
66
+ end
67
+
68
+ def infix_flatten(left, _token)
69
+ base = node(:flatten, nil, [left])
70
+ node(:projection, nil, [base, projection_rhs(9)])
71
+ end
72
+
73
+ def prefix_filter(token)
74
+ infix_filter(node(:current), token)
75
+ end
76
+
77
+ def infix_filter(left, _token)
78
+ condition = expression
79
+ expect(:rbracket)
80
+ node(:filter_projection, nil, [left, projection_rhs(21), condition])
81
+ end
82
+
83
+ def projection_rhs(power)
84
+ return node(:current) if POWER.fetch(peek.type, 0) < 10
85
+ return expression(power) if %i[lbracket filter].include?(peek.type)
86
+ return dot_rhs(power) if peek.type == :dot && advance
87
+
88
+ syntax!("Invalid projection")
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Parser
6
+ POWER = {
7
+ pipe: 1, or: 2, and: 3, eq: 5, ne: 5, lt: 5, lte: 5, gt: 5, gte: 5,
8
+ flatten: 9, filter: 21, dot: 40, lbracket: 55, lparen: 60
9
+ }.freeze
10
+ PREFIX = {
11
+ identifier: :prefix_identifier, literal: :prefix_literal, current: :prefix_current,
12
+ lparen: :prefix_group, lbracket: :prefix_bracket, lbrace: :prefix_hash,
13
+ not: :prefix_not, expref: :prefix_reference, star: :prefix_star,
14
+ flatten: :prefix_flatten, filter: :prefix_filter
15
+ }.freeze
16
+ INFIX = {
17
+ dot: :infix_dot, lbracket: :infix_bracket, lparen: :infix_function,
18
+ flatten: :infix_flatten, filter: :infix_filter
19
+ }.freeze
20
+
21
+ def initialize(source, max_depth: 128)
22
+ raise ArgumentError, "max_depth must be an integer from 1 through 128" unless (1..128).cover?(max_depth)
23
+
24
+ lexer = Lexer.new(source, max_depth: max_depth)
25
+ @source = lexer.source
26
+ @tokens = lexer.tokens
27
+ @index = 0
28
+ @level = 0
29
+ @max_depth = max_depth
30
+ end
31
+
32
+ def parse
33
+ result = expression
34
+ expect(:eof)
35
+ result
36
+ end
37
+
38
+ private
39
+
40
+ def expression(power = 0)
41
+ @level += 1
42
+ syntax!("Expression exceeds nesting limit") if @level > @max_depth + 1
43
+ token = advance
44
+ method = PREFIX[token.type]
45
+ syntax!("Expected an expression", token) unless method
46
+ left = send(method, token)
47
+ left = infix(left, advance) while power < POWER.fetch(peek.type, 0)
48
+ left
49
+ ensure
50
+ @level -= 1
51
+ end
52
+
53
+ def infix(left, token)
54
+ method = INFIX[token.type]
55
+ return send(method, left, token) if method
56
+
57
+ right = expression(POWER.fetch(token.type))
58
+ return sequence(token.type, left, right) if %i[pipe and or].include?(token.type)
59
+
60
+ node(token.type, nil, [left, right])
61
+ end
62
+
63
+ def sequence(kind, left, right)
64
+ children = left.kind == kind ? [*left.children, right] : [left, right]
65
+ node(kind, nil, children)
66
+ end
67
+
68
+ def node(kind, value = nil, children = [], start: nil)
69
+ result = AST::Node.new(
70
+ kind: kind, value: value, children: children,
71
+ start: start || children.first&.start || previous.start, finish: previous.finish
72
+ )
73
+ syntax!("Expression exceeds nesting limit") if result.depth > @max_depth
74
+ result
75
+ end
76
+
77
+ def peek
78
+ @tokens.fetch(@index)
79
+ end
80
+
81
+ def previous
82
+ @tokens.fetch([@index - 1, 0].max)
83
+ end
84
+
85
+ def advance
86
+ token = peek
87
+ @index += 1 unless token.type == :eof
88
+ token
89
+ end
90
+
91
+ def expect(type)
92
+ token = advance
93
+ syntax!("Expected #{type}, received #{token.type}", token) unless token.type == type
94
+ token
95
+ end
96
+
97
+ def separated(terminator, empty: false)
98
+ # @type var result: Array[AST::Node]
99
+ result = []
100
+ unless empty && peek.type == terminator
101
+ loop do
102
+ result << yield
103
+ break unless peek.type == :comma
104
+
105
+ advance
106
+ end
107
+ end
108
+ expect(terminator)
109
+ result
110
+ end
111
+
112
+ def syntax!(message, token = peek)
113
+ raise Errors::SyntaxError.new(message, expression: @source, position: token.start)
114
+ end
115
+ end
116
+ end
117
+
118
+ require_relative "parser/expressions"
119
+ require_relative "parser/projections"