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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Evaluator
6
+ COMPARISONS = { lt: :<, lte: :<=, gt: :>, gte: :>= }.freeze
7
+
8
+ private
9
+
10
+ def boolean(node, data)
11
+ # @type var result: value
12
+ result = nil
13
+ node.children.each do |child|
14
+ result = evaluate(child, data)
15
+ break if node.kind == :or ? !Types.falsey?(result) : Types.falsey?(result)
16
+ end
17
+ result
18
+ end
19
+
20
+ def negation?(node, data)
21
+ Types.falsey?(evaluate(node.children.fetch(0), data))
22
+ end
23
+
24
+ def equality(node, data)
25
+ left, right = node.children.map { |child| evaluate(child, data) }
26
+ result = Types.equal?(left, right)
27
+ node.kind == :eq ? result : !result
28
+ end
29
+
30
+ def ordering(node, data)
31
+ left, right = node.children.map { |child| evaluate(child, data) }
32
+ type = Types.name(left)
33
+ return nil unless type == Types.name(right) && %i[number string].include?(type)
34
+
35
+ if type == :string
36
+ left = Functions::Coercions.string(left)
37
+ right = Functions::Coercions.string(right)
38
+ end
39
+ left.public_send(COMPARISONS.fetch(node.kind), right)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Evaluator
6
+ private
7
+
8
+ def projection(node, data)
9
+ values = DataAccess.array(evaluate(node.children.fetch(0), data))
10
+ project(node, values)
11
+ end
12
+
13
+ def value_projection(node, data)
14
+ mapping = DataAccess.object(evaluate(node.children.fetch(0), data))
15
+ project(node, mapping&.values)
16
+ end
17
+
18
+ def filter_projection(node, data)
19
+ values = DataAccess.array(evaluate(node.children.fetch(0), data))
20
+ selected = values&.reject { |value| Types.falsey?(evaluate(node.children.fetch(2), value)) }
21
+ return nil unless selected
22
+
23
+ child = node.children.fetch(1)
24
+ selected.map! { |value| evaluate(child, value) }
25
+ selected.compact!
26
+ selected
27
+ end
28
+
29
+ def project(node, values)
30
+ return nil unless values
31
+
32
+ child = node.children.fetch(1)
33
+ result = values.map { |value| evaluate(child, value) }
34
+ result.compact!
35
+ result
36
+ end
37
+
38
+ def flatten(node, data)
39
+ values = DataAccess.array(evaluate(node.children.fetch(0), data))
40
+ values&.flat_map { |value| DataAccess.array(value) || [value] }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Evaluator
6
+ private
7
+
8
+ def literal(node, _data)
9
+ copy_literal(node.value)
10
+ end
11
+
12
+ def copy_literal(value)
13
+ case value
14
+ when Array then value.map { |item| copy_literal(item) }
15
+ when Hash then value.transform_values { |item| copy_literal(item) }
16
+ when String then value.dup
17
+ else value
18
+ end
19
+ end
20
+
21
+ def index(node, data)
22
+ array = DataAccess.array(evaluate(node.children.fetch(0), data))
23
+ position = node.value
24
+ raise TypeError, "Invalid array index" unless position.is_a?(Integer)
25
+
26
+ return nil unless array && position.between?(-array.length, array.length - 1)
27
+
28
+ array[position]
29
+ end
30
+
31
+ def group(node, data)
32
+ evaluate(node.children.fetch(0), data)
33
+ end
34
+
35
+ def sequence(node, data)
36
+ node.children.each { |child| data = evaluate(child, data) }
37
+ data
38
+ end
39
+
40
+ def reference(node, _data)
41
+ ExpressionReference.new { |value| evaluate(node.children.fetch(0), value) }
42
+ end
43
+
44
+ def function(node, data)
45
+ arguments = node.children.map { |child| evaluate(child, data) }
46
+ @functions.call(node.value.to_s, arguments, disable_visit_errors: @disable_visit_errors)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Evaluator
6
+ private
7
+
8
+ def slice(node, data)
9
+ values = DataAccess.array(evaluate(node.children.fetch(0), data))
10
+ return nil unless values
11
+
12
+ start, stop, step = Array(node.value).map { |part| part.nil? ? nil : Integer(part) }
13
+ Slice.new(start: start, stop: stop, step: step).apply(values)
14
+ end
15
+ end
16
+
17
+ # @api private
18
+ class Slice
19
+ def initialize(start:, stop:, step:)
20
+ @start = start
21
+ @stop = stop
22
+ @step = step || 1
23
+ raise Errors::InvalidValueError, "Slice step cannot be zero" if @step.zero?
24
+
25
+ @forward = @step.positive?
26
+ end
27
+
28
+ def apply(values)
29
+ length = values.length
30
+ index = bound(@start, length, @forward ? 0 : length - 1)
31
+ stop = bound(@stop, length, @forward ? length : -1)
32
+ # @type var result: Array[value]
33
+ result = []
34
+ while @forward ? index < stop : index > stop
35
+ result << values[index]
36
+ index += @step
37
+ end
38
+ result
39
+ end
40
+
41
+ private
42
+
43
+ def bound(value, length, fallback)
44
+ return fallback if value.nil?
45
+
46
+ index = value.negative? ? value + length : value
47
+ @forward ? index.clamp(0, length) : index.clamp(-1, length - 1)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Evaluator
6
+ HANDLERS = {
7
+ literal: :literal, index: :index,
8
+ group: :group, subexpression: :sequence, pipe: :sequence,
9
+ and: :boolean, or: :boolean, not: :negation?,
10
+ eq: :equality, ne: :equality, lt: :ordering, lte: :ordering, gt: :ordering, gte: :ordering,
11
+ multi_select_list: :list, multi_select_hash: :mapping,
12
+ function: :function, expression_reference: :reference,
13
+ projection: :projection, value_projection: :value_projection,
14
+ filter_projection: :filter_projection, flatten: :flatten, slice: :slice
15
+ }.freeze
16
+
17
+ def initialize(functions:, disable_visit_errors: false)
18
+ @functions = functions
19
+ @disable_visit_errors = disable_visit_errors
20
+ freeze
21
+ end
22
+
23
+ def evaluate(node, data)
24
+ case node.kind
25
+ when :field then DataAccess.field(data, node.value.to_s)
26
+ when :current then data
27
+ else send(HANDLERS.fetch(node.kind), node, data)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ require_relative "evaluation/scalars"
34
+ require_relative "evaluation/logic"
35
+ require_relative "evaluation/construction"
36
+ require_relative "evaluation/projections"
37
+ require_relative "evaluation/slices"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # Immutable parsed syntax that can search multiple input documents.
5
+ class Expression
6
+ DEFAULT_EVALUATOR = Evaluator.new(functions: Functions.default)
7
+ # @return [String] an independent, frozen expression string
8
+ attr_reader :source
9
+
10
+ # @api private
11
+ def initialize(source, root, max_depth: 128, options: Options::DEFAULT)
12
+ @source = source.dup.freeze
13
+ @root = root
14
+ @options = options
15
+ @plan = Plan.build(root, options)
16
+ @max_depth = max_depth
17
+ freeze
18
+ end
19
+
20
+ # Search input with this parsed expression.
21
+ # @param data [Object] Ruby values, Pathname, IO, or StringIO
22
+ # @param options [Hash] positional compatibility options
23
+ # @param keywords [Hash] keyword evaluation options
24
+ # @return [Object] the query result, including nil
25
+ def search(data, options = Options::EMPTY, **keywords)
26
+ if options.equal?(Options::EMPTY) && keywords.empty?
27
+ return @plan.call(data.is_a?(Hash) ? data : Input.load(data))
28
+ end
29
+
30
+ config = Options.new(options, keywords, default_depth: @max_depth, defaults: @options)
31
+ evaluate(Input.load(data), config)
32
+ end
33
+
34
+ # Search input using the same options and behavior as {#search}.
35
+ # @return [Object] the query result, including nil
36
+ alias call search
37
+
38
+ # @api private
39
+ def configured(options)
40
+ return self if options.functions.equal?(Functions.default) && !options.disable_visit_errors
41
+
42
+ self.class.new(source, @root, max_depth: @max_depth, options: options)
43
+ end
44
+
45
+ # @api private
46
+ def evaluate(data, config)
47
+ if @root.depth > config.max_depth
48
+ raise Errors::SyntaxError.new("Expression exceeds nesting limit", expression: source)
49
+ end
50
+
51
+ return @plan.call(data) if config.equal?(@options) || configured_for?(config)
52
+
53
+ evaluator = Evaluator.new(functions: config.functions, disable_visit_errors: config.disable_visit_errors)
54
+ evaluator.evaluate(@root, data)
55
+ end
56
+
57
+ private
58
+
59
+ def configured_for?(config)
60
+ config.functions.equal?(@options.functions) && config.disable_visit_errors == @options.disable_visit_errors
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class ExpressionReference
6
+ def initialize(&block)
7
+ @block = block
8
+ freeze
9
+ end
10
+
11
+ def call(value)
12
+ @block.call(value)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ module Coercions
7
+ extend self
8
+
9
+ def number(value)
10
+ return value if value.is_a?(Integer)
11
+ return value if value.is_a?(Float) && value.finite?
12
+
13
+ raise Signature::TypeMismatch.new("Expected a finite number", origin: validation_origin)
14
+ end
15
+
16
+ def string(value)
17
+ DataAccess.string(value) || raise(Signature::TypeMismatch.new("Expected a string", origin: validation_origin))
18
+ end
19
+
20
+ def array(value)
21
+ DataAccess.array(value) || raise(Signature::TypeMismatch.new("Expected an array", origin: validation_origin))
22
+ end
23
+
24
+ def reference(value)
25
+ return value if value.is_a?(ExpressionReference)
26
+
27
+ raise Signature::TypeMismatch.new("Expected an expression reference", origin: validation_origin)
28
+ end
29
+
30
+ def object(value)
31
+ DataAccess.object(value) || raise(Signature::TypeMismatch.new("Expected an object", origin: validation_origin))
32
+ end
33
+
34
+ private
35
+
36
+ def validation_origin
37
+ nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # Standard and custom function definitions.
5
+ class Functions
6
+ # @api private
7
+ module Collections
8
+ SIGNATURES = {
9
+ "avg" => [[:array_number]], "sum" => [[:array_number]],
10
+ "min" => [%i[array_number array_string]], "max" => [%i[array_number array_string]],
11
+ "sort" => [%i[array_number array_string]], "join" => [[:string], [:array_string]],
12
+ "keys" => [[:object]], "values" => [[:object]], "merge" => [[:object]],
13
+ "map" => [[:expref], [:array]], "sort_by" => [[:array], [:expref]],
14
+ "min_by" => [[:array], [:expref]], "max_by" => [[:array], [:expref]]
15
+ }.freeze
16
+
17
+ def self.register(registry)
18
+ SIGNATURES.reduce(registry) do |result, (name, signature)|
19
+ result.with_builtin(name, signature: signature, handler: name.to_sym, variadic: name == "merge")
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def sum(value)
26
+ array(value).sum { |item| number(item) }
27
+ end
28
+
29
+ def avg(value)
30
+ values = array(value)
31
+ number(sum(value)).fdiv(values.length) unless values.empty?
32
+ end
33
+
34
+ def min(value)
35
+ array(value).min_by { |item| sort_key(item) }
36
+ end
37
+
38
+ def max(value)
39
+ array(value).max_by { |item| sort_key(item) }
40
+ end
41
+
42
+ def sort(value)
43
+ array(value).each_with_index.sort_by do |item, index|
44
+ [sort_key(item), index]
45
+ end.map(&:first)
46
+ end
47
+
48
+ def join(separator, value)
49
+ array(value).map do |item|
50
+ string(item)
51
+ end.join(string(separator))
52
+ end
53
+
54
+ def keys(value)
55
+ object(value).keys.map(&:to_s)
56
+ end
57
+
58
+ def values(value)
59
+ object(value).values
60
+ end
61
+
62
+ def merge(*values)
63
+ # @type var result: Hash[value, value]
64
+ result = {}
65
+ values.each { |value| result.merge!(object(value)) }
66
+ result
67
+ end
68
+
69
+ def sort_key(value)
70
+ DataAccess.string(value) || number(value)
71
+ end
72
+ end
73
+
74
+ # @api private
75
+ DEFAULT = Collections.register(Scalar.register(new))
76
+
77
+ # @return [Functions] immutable standard function definitions
78
+ def self.default
79
+ DEFAULT
80
+ end
81
+ end
82
+ end
83
+
84
+ require_relative "references"
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ module Scalar
7
+ NUMBER = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/
8
+
9
+ private
10
+
11
+ def to_array(value)
12
+ DataAccess.array(value) || [value]
13
+ end
14
+
15
+ def to_string(value)
16
+ DataAccess.string(value) || JSON.generate(Types.normalize(value))
17
+ rescue JSON::NestingError
18
+ raise Errors::InvalidValueError, "Value is too deeply nested to serialize"
19
+ end
20
+
21
+ def to_number(value)
22
+ return value if Types.number?(value)
23
+
24
+ text = DataAccess.string(value)&.strip
25
+ return nil unless text && NUMBER.match?(text)
26
+
27
+ number = Integer(text, 10, exception: false) || Float(text, exception: false)
28
+ number if Types.number?(number)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ class FunctionCall
7
+ include Scalar
8
+ include Collections
9
+ include Coercions
10
+
11
+ private :number, :string, :array, :reference, :object
12
+
13
+ def call(definition, name, arguments, disable_visit_errors: false)
14
+ definition.signature.validate!(arguments, name, origin: self)
15
+ implementation = definition.implementation
16
+ implementation.is_a?(Symbol) ? send(implementation, *arguments) : implementation.call(*arguments)
17
+ rescue Signature::TypeMismatch, Signature::ArityMismatch => e
18
+ raise unless disable_visit_errors && e.origin.equal?(self)
19
+
20
+ nil
21
+ end
22
+
23
+ STRICT = new.freeze
24
+
25
+ private
26
+
27
+ def validation_origin
28
+ self
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ module Collections
7
+ SORTABLE_TYPES = %i[number string].freeze
8
+
9
+ private
10
+
11
+ def map(expression, value)
12
+ reference = reference(expression)
13
+ array(value).map { |item| reference.call(item) }
14
+ end
15
+
16
+ def sort_by(value, expression)
17
+ reference = reference(expression)
18
+ # @type var expected: Symbol?
19
+ expected = nil
20
+ index = 0
21
+ array(value).sort_by do |item|
22
+ key = reference.call(item)
23
+ expected = checked_type(key, expected)
24
+ index += 1
25
+ [sort_key(key), index]
26
+ end
27
+ end
28
+
29
+ def min_by(value, expression)
30
+ keyed(value, expression).min_by(&:first)&.last
31
+ end
32
+
33
+ def max_by(value, expression)
34
+ keyed(value, expression).max_by(&:first)&.last
35
+ end
36
+
37
+ def keyed(value, expression)
38
+ reference = reference(expression)
39
+ # @type var expected: Symbol?
40
+ expected = nil
41
+ array(value).map do |item|
42
+ key = reference.call(item)
43
+ expected = checked_type(key, expected)
44
+ [sort_key(key), item]
45
+ end
46
+ end
47
+
48
+ def checked_type(value, expected)
49
+ type = Types.name(value)
50
+ unless SORTABLE_TYPES.include?(type)
51
+ raise Signature::TypeMismatch.new("Expression keys must be numbers or strings", origin: validation_origin)
52
+ end
53
+ if expected && expected != type
54
+ raise Signature::TypeMismatch.new("Expression keys must have one type",
55
+ origin: validation_origin)
56
+ end
57
+
58
+ type
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ module Scalar
7
+ SIGNATURES = {
8
+ "abs" => [[:number]], "ceil" => [[:number]], "floor" => [[:number]],
9
+ "contains" => [%i[array string], [:any]],
10
+ "ends_with" => [[:string], [:string]], "starts_with" => [[:string], [:string]],
11
+ "length" => [%i[string array object]], "reverse" => [%i[string array]],
12
+ "type" => [[:any]], "to_array" => [[:any]], "to_string" => [[:any]],
13
+ "to_number" => [[:any]], "not_null" => [[:any]]
14
+ }.freeze
15
+
16
+ def self.register(registry)
17
+ SIGNATURES.reduce(registry) do |result, (name, signature)|
18
+ handler = %w[contains ends_with starts_with].include?(name) ? "#{name}?" : name
19
+ result.with_builtin(name, signature: signature, handler: handler.to_sym, variadic: name == "not_null")
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def abs(value)
26
+ number(value).abs
27
+ end
28
+
29
+ def ceil(value)
30
+ number(value).ceil
31
+ end
32
+
33
+ def floor(value)
34
+ number(value).floor
35
+ end
36
+
37
+ def contains?(subject, search)
38
+ text = DataAccess.string(subject)
39
+ return text.include?(string(search)) if text
40
+
41
+ array(subject).any? { |item| Types.equal?(item, search) }
42
+ end
43
+
44
+ def ends_with?(text, suffix)
45
+ string(text).end_with?(string(suffix))
46
+ end
47
+
48
+ def starts_with?(text, prefix)
49
+ string(text).start_with?(string(prefix))
50
+ end
51
+
52
+ def length(value)
53
+ collection = DataAccess.string(value) || DataAccess.array(value) || object(value)
54
+ collection.length
55
+ end
56
+
57
+ def reverse(value)
58
+ (DataAccess.string(value) || array(value)).reverse
59
+ end
60
+
61
+ def type(value)
62
+ name = Types.name(value)
63
+ name.to_s unless name == :unknown
64
+ end
65
+
66
+ def not_null(*values)
67
+ values.find { |value| !value.nil? }
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ require_relative "coercions"
74
+ require_relative "conversion"