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,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ module Plan
6
+ BUILDERS = {
7
+ field: :field, current: :current, group: :group,
8
+ subexpression: :sequence, pipe: :sequence, index: :index,
9
+ multi_select_list: :list, projection: :projection,
10
+ value_projection: :value_projection, slice: :slice, function: :function,
11
+ expression_reference: :reference
12
+ }.freeze
13
+
14
+ def self.build(node, options = Options::DEFAULT)
15
+ Builder.new(options).build(node)
16
+ end
17
+
18
+ # @api private
19
+ class Builder
20
+ def initialize(options)
21
+ @functions = options.functions
22
+ @disable_visit_errors = options.disable_visit_errors
23
+ @evaluator = if @functions.equal?(Functions.default) && !@disable_visit_errors
24
+ Expression::DEFAULT_EVALUATOR
25
+ else
26
+ Evaluator.new(functions: @functions, disable_visit_errors: @disable_visit_errors)
27
+ end
28
+ freeze
29
+ end
30
+
31
+ def build(node)
32
+ builder = BUILDERS[node.kind]
33
+ (builder ? send(builder, node) : fallback(node)).freeze
34
+ end
35
+
36
+ private
37
+
38
+ def field(node)
39
+ DataAccess::FieldReader.new(node.value.to_s)
40
+ end
41
+
42
+ def current(_node)
43
+ ->(data) { data }
44
+ end
45
+
46
+ def group(node)
47
+ build(node.children.fetch(0))
48
+ end
49
+
50
+ def sequence(node)
51
+ children = node.children.map { |child| build(child) }.freeze
52
+ lambda do |data|
53
+ children.each { |child| data = child.call(data) }
54
+ data
55
+ end
56
+ end
57
+
58
+ def index(node)
59
+ child = build(node.children.fetch(0))
60
+ position = Integer(node.value) # : Integer
61
+ lambda do |data|
62
+ values = DataAccess.array(child.call(data))
63
+ values[position] if values && position.between?(-values.length, values.length - 1)
64
+ end
65
+ end
66
+
67
+ def list(node)
68
+ children = node.children.map { |child| build(child) }.freeze
69
+ ->(data) { children.map { |child| child.call(data) } unless data.nil? }
70
+ end
71
+
72
+ def projection(node)
73
+ child = build(node.children.fetch(0))
74
+ project = projector(node.children.fetch(1))
75
+ ->(data) { project.call(DataAccess.array(child.call(data))) }
76
+ end
77
+
78
+ def value_projection(node)
79
+ child = build(node.children.fetch(0))
80
+ project = projector(node.children.fetch(1))
81
+ ->(data) { project.call(DataAccess.object(child.call(data))&.values) }
82
+ end
83
+
84
+ def projector(node)
85
+ child = build(node)
86
+ lambda do |values|
87
+ if values
88
+ result = values.map { |value| child.call(value) }
89
+ result.compact!
90
+ result
91
+ end
92
+ end
93
+ end
94
+
95
+ def slice(node)
96
+ child = build(node.children.fetch(0))
97
+ start, stop, step = Array(node.value).map { |part| part.nil? ? nil : Integer(part) }
98
+ slice = Slice.new(start: start, stop: stop, step: step).freeze
99
+ lambda do |data|
100
+ values = DataAccess.array(child.call(data))
101
+ slice.apply(values) if values
102
+ end
103
+ end
104
+
105
+ def function(node)
106
+ name = node.value.to_s
107
+ unless !@disable_visit_errors && %w[length reverse].include?(name) && node.children.length == 1 &&
108
+ native_function?(name)
109
+ return function_call(name, node.children)
110
+ end
111
+
112
+ string_function(name, build(node.children.fetch(0)))
113
+ end
114
+
115
+ def native_function?(name)
116
+ @functions.instance_of?(Functions) && @functions.definition(name).equal?(Functions.default.definition(name))
117
+ end
118
+
119
+ def string_function(name, child)
120
+ lambda do |data|
121
+ value = child.call(data)
122
+ if value.is_a?(String)
123
+ name == "length" ? value.length : value.reverse
124
+ else
125
+ Functions.default.call(name, [value])
126
+ end
127
+ end
128
+ end
129
+
130
+ def fallback(node)
131
+ evaluator = @evaluator
132
+ ->(data) { evaluator.evaluate(node, data) }
133
+ end
134
+
135
+ def function_call(name, nodes)
136
+ arguments = nodes.map { |node| build(node) }.freeze
137
+ functions = @functions
138
+ if @disable_visit_errors
139
+ lambda do |data|
140
+ functions.call(name, arguments.map { |argument| argument.call(data) }, disable_visit_errors: true)
141
+ end
142
+ else
143
+ ->(data) { functions.call(name, arguments.map { |argument| argument.call(data) }) }
144
+ end
145
+ end
146
+
147
+ def reference(node)
148
+ operation = build(node.children.fetch(0))
149
+ ->(_data) { ExpressionReference.new { |value| operation.call(value) } }
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ Token = Data.define(:type, :value, :start, :finish)
6
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ module Types
6
+ class << self
7
+ def name(value)
8
+ case value
9
+ when Integer then :number
10
+ when String then :string
11
+ when NilClass then :null
12
+ when TrueClass, FalseClass then :boolean
13
+ else other_name(value)
14
+ end
15
+ end
16
+
17
+ def number?(value)
18
+ value.is_a?(Integer) || (value.is_a?(Float) && value.finite?)
19
+ end
20
+
21
+ def falsey?(value)
22
+ return true if value.nil? || value.equal?(false)
23
+ return false if value.equal?(true)
24
+
25
+ collection = DataAccess.string(value) || DataAccess.array(value) || DataAccess.object(value)
26
+ collection ? collection.empty? : false
27
+ end
28
+
29
+ def equal?(left, right)
30
+ pending = [[normalize(left), normalize(right)]]
31
+ while (pair = pending.pop)
32
+ return false unless matching?(pair.fetch(0), pair.fetch(1), pending)
33
+ end
34
+ true
35
+ end
36
+
37
+ def normalize(value)
38
+ Normalizer.new.call(value)
39
+ end
40
+
41
+ private
42
+
43
+ def other_name(value)
44
+ return :number if number?(value)
45
+ return :expref if value.is_a?(ExpressionReference)
46
+
47
+ collection_name(value)
48
+ end
49
+
50
+ def collection_name(value)
51
+ return :string if DataAccess.string(value)
52
+ return :array if DataAccess.array(value)
53
+ return :object if DataAccess.object(value)
54
+
55
+ :unknown
56
+ end
57
+
58
+ def matching?(left, right, pending)
59
+ case left
60
+ when Array then arrays_match?(left, right, pending)
61
+ when Hash then objects_match?(left, right, pending)
62
+ else left == right
63
+ end
64
+ end
65
+
66
+ def arrays_match?(left, right, pending)
67
+ return false unless right.is_a?(Array) && left.length == right.length
68
+
69
+ left.each_with_index { |value, index| pending << [value, right[index]] }
70
+ true
71
+ end
72
+
73
+ def objects_match?(left, right, pending)
74
+ return false unless right.is_a?(Hash) && left.length == right.length
75
+ return false unless left.each_key.all? { |key| right.key?(key) }
76
+
77
+ left.each { |key, value| pending << [value, right[key]] }
78
+ true
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ require_relative "normalizer"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Queries JSON-compatible Ruby values using JMESPath expressions.
4
+ module JMESPath
5
+ # The semantic version of this implementation.
6
+ VERSION = "0.1.0"
7
+ end
data/lib/jmespath.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "jmespath/version"
4
+ require_relative "jmespath/errors"
5
+ require_relative "jmespath/token"
6
+ require_relative "jmespath/lexer"
7
+ require_relative "jmespath/expression_reference"
8
+ require_relative "jmespath/data_access"
9
+ require_relative "jmespath/types"
10
+ require_relative "jmespath/ast"
11
+ require_relative "jmespath/parser"
12
+ require_relative "jmespath/functions"
13
+ require_relative "jmespath/evaluator"
14
+ require_relative "jmespath/functions/scalar"
15
+ require_relative "jmespath/functions/collections"
16
+ require_relative "jmespath/functions/function_call"
17
+ require_relative "jmespath/options"
18
+ require_relative "jmespath/input"
19
+ require_relative "jmespath/cache"
20
+ require_relative "jmespath/plan"
21
+ require_relative "jmespath/expression"
22
+
23
+ # Search JSON-compatible Ruby values with JMESPath expressions.
24
+ module JMESPath
25
+ # @api private
26
+ CACHE = Cache.new
27
+ private_constant :CACHE
28
+
29
+ class << self
30
+ # Search Ruby data using a JMESPath expression.
31
+ # @param expression [String] query text
32
+ # @param data [Object] Ruby values, Pathname, IO, or StringIO
33
+ # @param options [Hash] positional compatibility options
34
+ # @param keywords [Hash] keyword evaluation options
35
+ # @option options [Boolean] :cache_expressions (true) reuse cached syntax
36
+ # @option options [Boolean] :disable_visit_errors (false) suppress argument type and arity failures
37
+ # @option options [Functions] :functions (Functions.default) registry for this search
38
+ # @option options [Integer] :max_depth (128) maximum expression nesting
39
+ # @raise [ArgumentError] options or expression type are invalid
40
+ # @raise [Errors::Error] the query cannot be parsed or evaluated
41
+ # @example Project a field from each item
42
+ # JMESPath.search("items[*].name", {items: [{name: "Ada"}]}) #=> ["Ada"]
43
+ # @return [Object] the query result, including nil
44
+ def search(expression, data, options = Options::EMPTY, **keywords)
45
+ config = if options.equal?(Options::EMPTY) && keywords.empty?
46
+ Options::DEFAULT
47
+ else
48
+ Options.new(options, keywords)
49
+ end
50
+ raise ArgumentError, "Expression must be a String" unless expression.is_a?(String)
51
+
52
+ compiled = compile_syntax(expression, config.cache_expressions, config.max_depth)
53
+ compiled.evaluate(Input.load(data), config)
54
+ end
55
+
56
+ # Parse an expression for repeated searches.
57
+ # @param expression [String] query text
58
+ # @param cache_expressions [Boolean] reuse bounded cached syntax
59
+ # @param max_depth [Integer] maximum nesting from 1 through 128
60
+ # @param functions [Functions] default registry for searches with this expression
61
+ # @param disable_visit_errors [Boolean] default suppression of argument type and arity failures
62
+ # @raise [ArgumentError] expression type or options are invalid
63
+ # @raise [Errors::SyntaxError] the expression is invalid or exceeds its nesting limit
64
+ # @example Reuse parsed syntax
65
+ # JMESPath.compile("name").search({name: "Ada"}) #=> "Ada"
66
+ # @return [Expression] immutable reusable syntax
67
+ def compile(expression, cache_expressions: true, max_depth: 128,
68
+ functions: Functions.default, disable_visit_errors: false)
69
+ raise ArgumentError, "Expression must be a String" unless expression.is_a?(String)
70
+
71
+ config = compilation_options(cache_expressions, max_depth, functions, disable_visit_errors)
72
+ compiled = compile_syntax(expression, cache_expressions, max_depth)
73
+ config.equal?(Options::DEFAULT) ? compiled : compiled.configured(config)
74
+ end
75
+
76
+ private
77
+
78
+ def compile_syntax(expression, cache, depth)
79
+ if cache
80
+ CACHE.fetch(expression, max_depth: depth) { |source| build_expression(source, depth) }
81
+ else
82
+ build_expression(expression, depth)
83
+ end
84
+ end
85
+
86
+ def compilation_options(cache, depth, functions, suppress)
87
+ if cache.equal?(true) && depth.equal?(128) && functions.equal?(Functions.default) && suppress.equal?(false)
88
+ return Options::DEFAULT
89
+ end
90
+
91
+ Options.new({ cache_expressions: cache, max_depth: depth,
92
+ functions: functions, disable_visit_errors: suppress })
93
+ end
94
+
95
+ def build_expression(expression, max_depth)
96
+ source = expression.dup.freeze
97
+ Expression.new(source, Parser.new(source, max_depth: max_depth).parse, max_depth: max_depth)
98
+ end
99
+ end
100
+ end
data/sig/jmespath.rbs ADDED
@@ -0,0 +1,55 @@
1
+ module JMESPath
2
+ VERSION: String
3
+ type value = Object?
4
+ type json = nil | bool | Integer | Float | String | Array[json] | Hash[String, json]
5
+
6
+ def self.search: (String, value, ?Hash[Symbol, value], **value) -> value
7
+ def self.compile: (String, ?cache_expressions: bool, ?max_depth: Integer, ?functions: Functions, ?disable_visit_errors: bool) -> Expression
8
+
9
+ module Errors
10
+ class Error < StandardError
11
+ end
12
+ class RuntimeError < Error
13
+ end
14
+ class SyntaxError < Error
15
+ attr_reader expression: String
16
+ attr_reader position: Integer
17
+ def initialize: (String, ?expression: String, ?position: Integer) -> void
18
+ def line: () -> Integer
19
+ def column: () -> Integer
20
+ end
21
+ class InvalidTypeError < RuntimeError
22
+ end
23
+ class InvalidValueError < RuntimeError
24
+ end
25
+ class InvalidArityError < RuntimeError
26
+ end
27
+ class UnknownFunctionError < RuntimeError
28
+ end
29
+ end
30
+
31
+ class Expression
32
+ attr_reader source: String
33
+ def search: (value, ?Hash[Symbol, value], **value) -> value
34
+ alias call search
35
+ end
36
+
37
+ class Functions
38
+ def initialize: () -> void
39
+ def self.default: () -> Functions
40
+ def with: (String, signature: Array[Array[Symbol]], ?variadic: bool, ?override: bool) ?{ (*value) -> value } -> Functions
41
+ def call: (String, Array[value], ?disable_visit_errors: bool) -> value
42
+ end
43
+
44
+ # Input stream protocol used by CLI.run.
45
+ interface _Reader
46
+ def read: () -> String?
47
+ end
48
+ # Output stream protocol used by CLI.run.
49
+ interface _Writer
50
+ def puts: (String) -> void
51
+ end
52
+ module CLI
53
+ def self.run: (Array[String], ?stdin: _Reader, ?stdout: _Writer, ?stderr: _Writer) -> Integer
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jmespath-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Leonid Svyatov
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.7'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ - !ruby/object:Gem::Dependency
33
+ name: stringio
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '3.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '4'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '3.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '4'
52
+ description: Search structured Ruby data with reusable JMESPath expressions.
53
+ executables:
54
+ - jmespath
55
+ - jmespath.rb
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - CHANGELOG.md
60
+ - CONTRIBUTING.md
61
+ - LICENSE
62
+ - LICENSES/python-MIT.txt
63
+ - LICENSES/ruby-Apache-2.0.txt
64
+ - README.md
65
+ - SECURITY.md
66
+ - VERSIONING.md
67
+ - docs/guides/compatibility.md
68
+ - docs/guides/compilation.md
69
+ - docs/guides/errors.md
70
+ - docs/guides/functions.md
71
+ - docs/guides/performance.md
72
+ - docs/guides/search.md
73
+ - exe/jmespath
74
+ - exe/jmespath.rb
75
+ - lib/jmespath.rb
76
+ - lib/jmespath/ast.rb
77
+ - lib/jmespath/cache.rb
78
+ - lib/jmespath/cli.rb
79
+ - lib/jmespath/data_access.rb
80
+ - lib/jmespath/errors.rb
81
+ - lib/jmespath/evaluation/construction.rb
82
+ - lib/jmespath/evaluation/logic.rb
83
+ - lib/jmespath/evaluation/projections.rb
84
+ - lib/jmespath/evaluation/scalars.rb
85
+ - lib/jmespath/evaluation/slices.rb
86
+ - lib/jmespath/evaluator.rb
87
+ - lib/jmespath/expression.rb
88
+ - lib/jmespath/expression_reference.rb
89
+ - lib/jmespath/functions.rb
90
+ - lib/jmespath/functions/coercions.rb
91
+ - lib/jmespath/functions/collections.rb
92
+ - lib/jmespath/functions/conversion.rb
93
+ - lib/jmespath/functions/function_call.rb
94
+ - lib/jmespath/functions/references.rb
95
+ - lib/jmespath/functions/scalar.rb
96
+ - lib/jmespath/functions/signature.rb
97
+ - lib/jmespath/input.rb
98
+ - lib/jmespath/lexer.rb
99
+ - lib/jmespath/normalizer.rb
100
+ - lib/jmespath/options.rb
101
+ - lib/jmespath/parser.rb
102
+ - lib/jmespath/parser/expressions.rb
103
+ - lib/jmespath/parser/projections.rb
104
+ - lib/jmespath/plan.rb
105
+ - lib/jmespath/token.rb
106
+ - lib/jmespath/types.rb
107
+ - lib/jmespath/version.rb
108
+ - sig/jmespath.rbs
109
+ homepage: https://github.com/svyatov/jmespath-ruby
110
+ licenses:
111
+ - MIT
112
+ metadata:
113
+ rubygems_mfa_required: 'true'
114
+ allowed_push_host: https://rubygems.org
115
+ source_code_uri: https://github.com/svyatov/jmespath-ruby
116
+ bug_tracker_uri: https://github.com/svyatov/jmespath-ruby/issues
117
+ changelog_uri: https://github.com/svyatov/jmespath-ruby/blob/main/CHANGELOG.md
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '3.3'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubygems_version: 4.0.20
133
+ specification_version: 4
134
+ summary: A typed Ruby implementation of the JMESPath query language
135
+ test_files: []