jmespath-ruby 0.1.1 → 0.1.2

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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ module Suggestions
7
+ class << self
8
+ def for(name, names)
9
+ names.filter_map { |candidate| ranked(name, candidate) }.sort.first(3).map(&:last)
10
+ end
11
+
12
+ private
13
+
14
+ def ranked(name, candidate)
15
+ threshold = [name.length, candidate.length].max <= 4 ? 1 : 2
16
+ return if (name.length - candidate.length).abs > threshold
17
+
18
+ score = distance(name, candidate)
19
+ [score, candidate] if score <= threshold
20
+ end
21
+
22
+ def distance(left, right)
23
+ left = left.chars
24
+ right = right.chars
25
+ previous = (0..right.length).to_a
26
+ older = previous
27
+ left.each_index do |index|
28
+ current = row(left, right, index, previous, older)
29
+ older = previous
30
+ previous = current
31
+ end
32
+ previous.fetch(right.length)
33
+ end
34
+
35
+ def row(left, right, index, previous, older)
36
+ current = [index + 1]
37
+ right.each_index do |column|
38
+ score = [previous.fetch(column + 1) + 1, current.fetch(column) + 1,
39
+ substitution(left, right, index, column, previous)]
40
+ score << (older.fetch(column - 1) + 1) if swap?(left, right, index, column)
41
+ current << score.min
42
+ end
43
+ current
44
+ end
45
+
46
+ def substitution(left, right, index, column, previous)
47
+ previous.fetch(column) + (left[index] == right[column] ? 0 : 1)
48
+ end
49
+
50
+ def swap?(left, right, index, column)
51
+ index.positive? && column.positive? && left[index] == right[column - 1] && left[index - 1] == right[column]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ class Functions
5
+ # @api private
6
+ class ValidationContext
7
+ def initialize(name, signature, source_range: nil, expected_types: nil, actual_type: nil)
8
+ @name = name.dup.freeze
9
+ @signature = signature
10
+ @source_range = source_range&.dup&.freeze
11
+ @expected_types = expected_types&.dup&.freeze
12
+ @actual_type = actual_type
13
+ freeze
14
+ end
15
+
16
+ def type_error(message, position:, expected_types:, actual_type:, origin:, stage: :argument) # rubocop:disable Metrics/ParameterLists
17
+ Signature::TypeMismatch.new(
18
+ message,
19
+ **metadata,
20
+ origin: origin, argument_position: position,
21
+ expected_types: @expected_types || expected_types,
22
+ actual_type: @actual_type || actual_type, validation_stage: stage
23
+ )
24
+ end
25
+
26
+ def arity_error(message, actual_arity:, origin:)
27
+ Signature::ArityMismatch.new(
28
+ message, **metadata, origin: origin,
29
+ expected_arity: @signature.arguments.length, actual_arity: actual_arity
30
+ )
31
+ end
32
+
33
+ def array_element(position)
34
+ with_types(@signature.arguments.fetch(position - 1), actual_type: :array)
35
+ end
36
+
37
+ def with_types(types, actual_type: nil)
38
+ self.class.new(@name, @signature, source_range: @source_range, expected_types: types, actual_type: actual_type)
39
+ end
40
+
41
+ private
42
+
43
+ def metadata
44
+ { function_name: @name, signature: @signature.arguments, variadic: @signature.variadic?,
45
+ source_range: @source_range }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -4,7 +4,31 @@ module JMESPath
4
4
  # Immutable definitions for functions available to an expression.
5
5
  class Functions
6
6
  # @api private
7
- Definition = Data.define(:signature, :implementation)
7
+ Definition = Data.define(:signature, :implementation, :description)
8
+
9
+ # Immutable public description of a registered function.
10
+ class Metadata
11
+ # @return [String] function identifier
12
+ attr_reader :name
13
+ # @return [Array<Array<Symbol>>] accepted types for each declared argument
14
+ attr_reader :signature
15
+ # @return [String, nil] optional description
16
+ attr_reader :description
17
+
18
+ # @api private
19
+ def initialize(name, signature, variadic, description)
20
+ @name = name.dup.freeze
21
+ @signature = signature.map { |types| types.dup.freeze }.freeze
22
+ @variadic = variadic
23
+ @description = description&.dup&.freeze
24
+ freeze
25
+ end
26
+
27
+ # @return [Boolean] whether the final required argument repeats
28
+ def variadic?
29
+ @variadic
30
+ end
31
+ end
8
32
 
9
33
  # Create an empty immutable function registry.
10
34
  def initialize
@@ -19,23 +43,42 @@ module JMESPath
19
43
  # @param signature [Array<Array<Symbol>>] accepted types for each argument
20
44
  # @param variadic [Boolean] repeat the final argument type
21
45
  # @param override [Boolean] explicitly replace a definition
46
+ # @param description [String, nil] optional description, replaced on override
22
47
  # @yieldparam arguments [Object] evaluated arguments
23
48
  # @yieldreturn [Object] the function result
24
49
  # @raise [ArgumentError] the definition is invalid or duplicates an existing name
25
50
  # @return [Functions] the extended registry
26
- def with(name, signature:, variadic: false, override: false, &block)
51
+ def with(name, signature:, variadic: false, override: false, description: nil, &block)
27
52
  raise ArgumentError, "A function block is required" unless block
53
+ raise ArgumentError, "Invalid function description" unless description.nil? || description.is_a?(String)
28
54
 
29
- definition = Definition.new(Signature.new(signature, variadic: variadic), block)
55
+ definition = Definition.new(Signature.new(signature, variadic: variadic), block, description&.dup&.freeze)
30
56
  add(name, definition, override: override)
31
57
  end
32
58
 
33
59
  # @api private
34
- def with_builtin(name, signature:, handler:, variadic: false)
35
- definition = Definition.new(Signature.new(signature, variadic: variadic), handler)
60
+ def with_builtin(name, signature:, handler:, description:, variadic: false)
61
+ definition = Definition.new(Signature.new(signature, variadic: variadic), handler, description.dup.freeze)
36
62
  add(name, definition, override: false)
37
63
  end
38
64
 
65
+ # List names in ascending, case-sensitive lexical order.
66
+ # @return [Array<String>] frozen names in a frozen array
67
+ def names
68
+ @definitions.keys.sort.freeze
69
+ end
70
+
71
+ # Inspect a definition without executing its implementation.
72
+ # @param name [String] function identifier
73
+ # @return [Metadata, nil] immutable metadata, or nil if absent
74
+ def metadata(name)
75
+ definition = @definitions[name]
76
+ return unless definition
77
+
78
+ signature = definition.signature
79
+ Metadata.new(name, signature.arguments, signature.variadic?, definition.description)
80
+ end
81
+
39
82
  # Evaluate a registered function with checked arguments.
40
83
  # @param name [String] function identifier
41
84
  # @param arguments [Array<Object>] evaluated values
@@ -45,9 +88,19 @@ module JMESPath
45
88
  # @raise [Errors::InvalidTypeError] argument types are incompatible
46
89
  # @raise [Errors::InvalidArityError] the number of arguments is invalid
47
90
  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)
91
+ invoke(name, arguments, disable_visit_errors: disable_visit_errors)
92
+ end
93
+
94
+ # @api private
95
+ def call_with_context(name, arguments, source_range:, disable_visit_errors: nil)
96
+ # Preserve compiled keyword omission separately from the evaluator's explicit false.
97
+ unless method(:call).owner.equal?(Functions)
98
+ return call(name, arguments) if disable_visit_errors.nil?
99
+
100
+ return call(name, arguments, disable_visit_errors: disable_visit_errors)
101
+ end
102
+
103
+ invoke(name, arguments, source_range: source_range, disable_visit_errors: !!disable_visit_errors)
51
104
  end
52
105
 
53
106
  # @api private
@@ -57,6 +110,16 @@ module JMESPath
57
110
 
58
111
  private
59
112
 
113
+ def invoke(name, arguments, disable_visit_errors:, source_range: nil)
114
+ definition = @definitions.fetch(name) do
115
+ raise Errors::UnknownFunctionError.new("Unknown function: #{name}", function_name: name,
116
+ suggestions: Suggestions.for(name, names))
117
+ end
118
+ context = ValidationContext.new(name, definition.signature, source_range: source_range)
119
+ invocation = disable_visit_errors ? FunctionCall.new : FunctionCall::STRICT
120
+ invocation.call(definition, name, arguments, disable_visit_errors: disable_visit_errors, context: context)
121
+ end
122
+
60
123
  def add(name, definition, override:)
61
124
  unless name.is_a?(String) && name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
62
125
  raise ArgumentError, "Invalid function name"
@@ -71,3 +134,4 @@ module JMESPath
71
134
  end
72
135
 
73
136
  require_relative "functions/signature"
137
+ require_relative "functions/validation_context"
data/lib/jmespath/plan.rb CHANGED
@@ -104,25 +104,26 @@ module JMESPath
104
104
 
105
105
  def function(node)
106
106
  name = node.value.to_s
107
+ source_range = (node.start...node.finish)
107
108
  unless !@disable_visit_errors && %w[length reverse].include?(name) && node.children.length == 1 &&
108
109
  native_function?(name)
109
- return function_call(name, node.children)
110
+ return function_call(name, node.children, source_range)
110
111
  end
111
112
 
112
- string_function(name, build(node.children.fetch(0)))
113
+ string_function(name, build(node.children.fetch(0)), source_range)
113
114
  end
114
115
 
115
116
  def native_function?(name)
116
117
  @functions.instance_of?(Functions) && @functions.definition(name).equal?(Functions.default.definition(name))
117
118
  end
118
119
 
119
- def string_function(name, child)
120
+ def string_function(name, child, source_range)
120
121
  lambda do |data|
121
122
  value = child.call(data)
122
123
  if value.is_a?(String)
123
124
  name == "length" ? value.length : value.reverse
124
125
  else
125
- Functions.default.call(name, [value])
126
+ Functions.default.call_with_context(name, [value], source_range: source_range)
126
127
  end
127
128
  end
128
129
  end
@@ -132,15 +133,20 @@ module JMESPath
132
133
  ->(data) { evaluator.evaluate(node, data) }
133
134
  end
134
135
 
135
- def function_call(name, nodes)
136
+ def function_call(name, nodes, source_range)
136
137
  arguments = nodes.map { |node| build(node) }.freeze
137
138
  functions = @functions
138
139
  if @disable_visit_errors
139
140
  lambda do |data|
140
- functions.call(name, arguments.map { |argument| argument.call(data) }, disable_visit_errors: true)
141
+ functions.call_with_context(name, arguments.map { |argument| argument.call(data) },
142
+ source_range: source_range, disable_visit_errors: true)
141
143
  end
142
144
  else
143
- ->(data) { functions.call(name, arguments.map { |argument| argument.call(data) }) }
145
+ lambda do |data|
146
+ functions.call_with_context(name, arguments.map do |argument|
147
+ argument.call(data)
148
+ end, source_range: source_range)
149
+ end
144
150
  end
145
151
  end
146
152
 
@@ -3,5 +3,5 @@
3
3
  # Queries JSON-compatible Ruby values using JMESPath expressions.
4
4
  module JMESPath
5
5
  # The semantic version of this implementation.
6
- VERSION = "0.1.1"
6
+ VERSION = "0.1.2"
7
7
  end
data/lib/jmespath.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "jmespath/version"
4
4
  require_relative "jmespath/errors"
5
+ require_relative "jmespath/diagnostic"
5
6
  require_relative "jmespath/token"
6
7
  require_relative "jmespath/lexer"
7
8
  require_relative "jmespath/expression_reference"
@@ -10,6 +11,7 @@ require_relative "jmespath/types"
10
11
  require_relative "jmespath/ast"
11
12
  require_relative "jmespath/parser"
12
13
  require_relative "jmespath/functions"
14
+ require_relative "jmespath/functions/suggestions"
13
15
  require_relative "jmespath/evaluator"
14
16
  require_relative "jmespath/functions/scalar"
15
17
  require_relative "jmespath/functions/collections"
data/sig/jmespath.rbs CHANGED
@@ -12,20 +12,48 @@ module JMESPath
12
12
  class RuntimeError < Error
13
13
  end
14
14
  class SyntaxError < Error
15
+ attr_reader reason: String
15
16
  attr_reader expression: String
16
17
  attr_reader position: Integer
17
18
  def initialize: (String, ?expression: String, ?position: Integer) -> void
19
+ def location_known?: () -> bool
18
20
  def line: () -> Integer
19
21
  def column: () -> Integer
20
22
  end
21
23
  class InvalidTypeError < RuntimeError
24
+ include FunctionFailure
25
+ attr_reader argument_position: Integer?
26
+ attr_reader expected_types: Array[Symbol]?
27
+ attr_reader actual_type: Symbol?
28
+ attr_reader validation_stage: Symbol?
29
+ def initialize: (?String?, ?argument_position: Integer?, ?expected_types: Array[Symbol]?, ?actual_type: Symbol?, ?validation_stage: Symbol?, ?function_name: String?, ?signature: Array[Array[Symbol]]?, ?variadic: bool?, ?source_range: Range[Integer]?) -> void
22
30
  end
23
31
  class InvalidValueError < RuntimeError
24
32
  end
25
33
  class InvalidArityError < RuntimeError
34
+ include FunctionFailure
35
+ attr_reader expected_arity: Integer?
36
+ attr_reader actual_arity: Integer?
37
+ def initialize: (?String?, ?expected_arity: Integer?, ?actual_arity: Integer?, ?function_name: String?, ?signature: Array[Array[Symbol]]?, ?variadic: bool?, ?source_range: Range[Integer]?) -> void
26
38
  end
27
39
  class UnknownFunctionError < RuntimeError
40
+ attr_reader function_name: String?
41
+ attr_reader suggestions: Array[String]
42
+ def initialize: (?String?, ?function_name: String?, ?suggestions: Array[String]) -> void
28
43
  end
44
+
45
+ module FunctionFailure
46
+ @variadic: bool?
47
+ attr_reader function_name: String?
48
+ attr_reader signature: Array[Array[Symbol]]?
49
+ attr_reader source_range: Range[Integer]?
50
+ def initialize: (?String?, ?function_name: String?, ?signature: Array[Array[Symbol]]?, ?variadic: bool?, ?source_range: Range[Integer]?) -> void
51
+ def variadic?: () -> bool?
52
+ end
53
+ end
54
+
55
+ module Diagnostic
56
+ def self.format: (Errors::SyntaxError) -> String
29
57
  end
30
58
 
31
59
  class Expression
@@ -35,9 +63,18 @@ module JMESPath
35
63
  end
36
64
 
37
65
  class Functions
66
+ class Metadata
67
+ attr_reader name: String
68
+ attr_reader signature: Array[Array[Symbol]]
69
+ attr_reader description: String?
70
+ def variadic?: () -> bool
71
+ end
72
+
38
73
  def initialize: () -> void
39
74
  def self.default: () -> Functions
40
- def with: (String, signature: Array[Array[Symbol]], ?variadic: bool, ?override: bool) ?{ (*value) -> value } -> Functions
75
+ def with: (String, signature: Array[Array[Symbol]], ?variadic: bool, ?override: bool, ?description: String?) ?{ (*value) -> value } -> Functions
76
+ def names: () -> Array[String]
77
+ def metadata: (String) -> Metadata?
41
78
  def call: (String, Array[value], ?disable_visit_errors: bool) -> value
42
79
  end
43
80
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jmespath-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -77,7 +77,9 @@ files:
77
77
  - lib/jmespath/ast.rb
78
78
  - lib/jmespath/cache.rb
79
79
  - lib/jmespath/cli.rb
80
+ - lib/jmespath/cli/failure_diagnostic.rb
80
81
  - lib/jmespath/data_access.rb
82
+ - lib/jmespath/diagnostic.rb
81
83
  - lib/jmespath/errors.rb
82
84
  - lib/jmespath/evaluation/construction.rb
83
85
  - lib/jmespath/evaluation/logic.rb
@@ -95,6 +97,8 @@ files:
95
97
  - lib/jmespath/functions/references.rb
96
98
  - lib/jmespath/functions/scalar.rb
97
99
  - lib/jmespath/functions/signature.rb
100
+ - lib/jmespath/functions/suggestions.rb
101
+ - lib/jmespath/functions/validation_context.rb
98
102
  - lib/jmespath/input.rb
99
103
  - lib/jmespath/lexer.rb
100
104
  - lib/jmespath/normalizer.rb