jmespath-ruby 0.1.0 → 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.
@@ -6,33 +6,49 @@ module JMESPath
6
6
  module Coercions
7
7
  extend self
8
8
 
9
- def number(value)
9
+ NATIVE_TYPES = { String => :string, NilClass => :null, TrueClass => :boolean, FalseClass => :boolean,
10
+ Array => :array, Hash => :object, ExpressionReference => :expref }.freeze
11
+
12
+ def number(value, context: nil, position: nil, stage: :argument)
10
13
  return value if value.is_a?(Integer)
11
14
  return value if value.is_a?(Float) && value.finite?
12
15
 
13
- raise Signature::TypeMismatch.new("Expected a finite number", origin: validation_origin)
16
+ raise type_failure(value, "Expected a finite number", [:number], context, position, stage)
14
17
  end
15
18
 
16
- def string(value)
17
- DataAccess.string(value) || raise(Signature::TypeMismatch.new("Expected a string", origin: validation_origin))
19
+ def string(value, context: nil, position: nil)
20
+ DataAccess.string(value) || raise(type_failure(value, "Expected a string", [:string], context, position))
18
21
  end
19
22
 
20
- def array(value)
21
- DataAccess.array(value) || raise(Signature::TypeMismatch.new("Expected an array", origin: validation_origin))
23
+ def array(value, context: nil, position: nil)
24
+ DataAccess.array(value) || raise(type_failure(value, "Expected an array", [:array], context, position))
22
25
  end
23
26
 
24
- def reference(value)
27
+ def reference(value, context: nil, position: nil)
25
28
  return value if value.is_a?(ExpressionReference)
26
29
 
27
- raise Signature::TypeMismatch.new("Expected an expression reference", origin: validation_origin)
30
+ raise type_failure(value, "Expected an expression reference", [:expref], context, position)
28
31
  end
29
32
 
30
- def object(value)
31
- DataAccess.object(value) || raise(Signature::TypeMismatch.new("Expected an object", origin: validation_origin))
33
+ def object(value, context: nil, position: nil)
34
+ DataAccess.object(value) || raise(type_failure(value, "Expected an object", [:object], context, position))
32
35
  end
33
36
 
34
37
  private
35
38
 
39
+ def type_failure(value, message, types, context, position, stage = :argument) # rubocop:disable Metrics/ParameterLists
40
+ return Signature::TypeMismatch.new(message, origin: validation_origin) unless context
41
+
42
+ context.type_error(message, position: position, expected_types: types, actual_type: native_type(value),
43
+ origin: validation_origin, stage: stage)
44
+ end
45
+
46
+ def native_type(value)
47
+ return :number if Types.number?(value)
48
+
49
+ NATIVE_TYPES.find { |klass, _type| value.is_a?(klass) }&.last || :unknown
50
+ end
51
+
36
52
  def validation_origin
37
53
  nil
38
54
  end
@@ -14,60 +14,84 @@ module JMESPath
14
14
  "min_by" => [[:array], [:expref]], "max_by" => [[:array], [:expref]]
15
15
  }.freeze
16
16
 
17
+ DESCRIPTIONS = {
18
+ "avg" => "Mean, or null for an empty array.",
19
+ "sum" => "Sum, including zero for an empty array.",
20
+ "min" => "Minimum, or null for an empty array.",
21
+ "max" => "Maximum, or null for an empty array.",
22
+ "sort" => "Stable sorted array of numbers or strings.",
23
+ "join" => "Join strings with a separator.",
24
+ "keys" => "Object keys as strings.",
25
+ "values" => "Object values.",
26
+ "merge" => "Merge objects, with later values winning.",
27
+ "map" => "Map an expression over an array, including null results.",
28
+ "sort_by" => "Stable array sorted by homogeneous expression results.",
29
+ "min_by" => "Input element with minimum expression result, or null for an empty array.",
30
+ "max_by" => "Input element with maximum expression result, or null for an empty array."
31
+ }.freeze
32
+
17
33
  def self.register(registry)
18
34
  SIGNATURES.reduce(registry) do |result, (name, signature)|
19
- result.with_builtin(name, signature: signature, handler: name.to_sym, variadic: name == "merge")
35
+ result.with_builtin(name, signature: signature, handler: name.to_sym,
36
+ description: DESCRIPTIONS.fetch(name), variadic: name == "merge")
20
37
  end
21
38
  end
22
39
 
23
40
  private
24
41
 
25
- def sum(value)
26
- array(value).sum { |item| number(item) }
42
+ def sum(value, context:)
43
+ element_context = context.array_element(1)
44
+ array(value, context: context, position: 1).sum { |item| number(item, context: element_context, position: 1) }
27
45
  end
28
46
 
29
- def avg(value)
30
- values = array(value)
31
- number(sum(value)).fdiv(values.length) unless values.empty?
47
+ def avg(value, context:)
48
+ values = array(value, context: context, position: 1)
49
+ return if values.empty?
50
+
51
+ number(sum(value, context: context), context: context.array_element(1), position: 1).fdiv(values.length)
32
52
  end
33
53
 
34
- def min(value)
35
- array(value).min_by { |item| sort_key(item) }
54
+ def min(value, context:)
55
+ element_context = context.array_element(1)
56
+ array(value, context: context, position: 1).min_by { |item| sort_key(item, context: element_context) }
36
57
  end
37
58
 
38
- def max(value)
39
- array(value).max_by { |item| sort_key(item) }
59
+ def max(value, context:)
60
+ element_context = context.array_element(1)
61
+ array(value, context: context, position: 1).max_by { |item| sort_key(item, context: element_context) }
40
62
  end
41
63
 
42
- def sort(value)
43
- array(value).each_with_index.sort_by do |item, index|
44
- [sort_key(item), index]
64
+ def sort(value, context:)
65
+ element_context = context.array_element(1)
66
+ array(value, context: context, position: 1).each_with_index.sort_by do |item, index|
67
+ [sort_key(item, context: element_context), index]
45
68
  end.map(&:first)
46
69
  end
47
70
 
48
- def join(separator, value)
49
- array(value).map do |item|
50
- string(item)
51
- end.join(string(separator))
71
+ def join(separator, value, context:)
72
+ element_context = context.array_element(2)
73
+ array(value, context: context, position: 2).map do |item|
74
+ string(item, context: element_context, position: 2)
75
+ end.join(string(separator, context: context, position: 1))
52
76
  end
53
77
 
54
- def keys(value)
55
- object(value).keys.map(&:to_s)
78
+ def keys(value, context:)
79
+ object(value, context: context, position: 1).keys.map(&:to_s)
56
80
  end
57
81
 
58
- def values(value)
59
- object(value).values
82
+ def values(value, context:)
83
+ object(value, context: context, position: 1).values
60
84
  end
61
85
 
62
- def merge(*values)
86
+ def merge(*values, context:)
63
87
  # @type var result: Hash[value, value]
64
88
  result = {}
65
- values.each { |value| result.merge!(object(value)) }
89
+ values.each_with_index { |value, index| result.merge!(object(value, context: context, position: index + 1)) }
66
90
  result
67
91
  end
68
92
 
69
- def sort_key(value)
70
- DataAccess.string(value) || number(value)
93
+ def sort_key(value, context:, position: 1, stage: :argument)
94
+ DataAccess.string(value) || number(value, context: context, position: position, stage: stage)
71
95
  end
72
96
  end
73
97
 
@@ -8,17 +8,17 @@ module JMESPath
8
8
 
9
9
  private
10
10
 
11
- def to_array(value)
11
+ def to_array(value, **)
12
12
  DataAccess.array(value) || [value]
13
13
  end
14
14
 
15
- def to_string(value)
15
+ def to_string(value, **)
16
16
  DataAccess.string(value) || JSON.generate(Types.normalize(value))
17
17
  rescue JSON::NestingError
18
18
  raise Errors::InvalidValueError, "Value is too deeply nested to serialize"
19
19
  end
20
20
 
21
- def to_number(value)
21
+ def to_number(value, **)
22
22
  return value if Types.number?(value)
23
23
 
24
24
  text = DataAccess.string(value)&.strip
@@ -10,10 +10,15 @@ module JMESPath
10
10
 
11
11
  private :number, :string, :array, :reference, :object
12
12
 
13
- def call(definition, name, arguments, disable_visit_errors: false)
14
- definition.signature.validate!(arguments, name, origin: self)
13
+ def call(definition, name, arguments, disable_visit_errors: false, context: nil)
14
+ context ||= ValidationContext.new(name, definition.signature)
15
+ definition.signature.validate!(arguments, name, origin: self, context: context)
15
16
  implementation = definition.implementation
16
- implementation.is_a?(Symbol) ? send(implementation, *arguments) : implementation.call(*arguments)
17
+ if implementation.is_a?(Symbol)
18
+ send(implementation, *arguments, context: context)
19
+ else
20
+ implementation.call(*arguments)
21
+ end
17
22
  rescue Signature::TypeMismatch, Signature::ArityMismatch => e
18
23
  raise unless disable_visit_errors && e.origin.equal?(self)
19
24
 
@@ -8,51 +8,68 @@ module JMESPath
8
8
 
9
9
  private
10
10
 
11
- def map(expression, value)
12
- reference = reference(expression)
13
- array(value).map { |item| reference.call(item) }
11
+ def map(expression, value, context:)
12
+ reference = reference(expression, context: context, position: 1)
13
+ array(value, context: context, position: 2).map { |item| reference.call(item) }
14
14
  end
15
15
 
16
- def sort_by(value, expression)
17
- reference = reference(expression)
16
+ def sort_by(value, expression, context:)
17
+ reference = reference(expression, context: context, position: 2)
18
18
  # @type var expected: Symbol?
19
19
  expected = nil
20
+ # @type var result_context: ValidationContext?
21
+ result_context = nil
20
22
  index = 0
21
- array(value).sort_by do |item|
23
+ array(value, context: context, position: 1).sort_by do |item|
22
24
  key = reference.call(item)
23
- expected = checked_type(key, expected)
25
+ type = checked_type(key, expected, context: context)
26
+ expected = type
27
+ key_context = result_context || context.with_types([type])
28
+ result_context = key_context
24
29
  index += 1
25
- [sort_key(key), index]
30
+ [sort_key(key, context: key_context, position: 2, stage: :expression_result), index]
26
31
  end
27
32
  end
28
33
 
29
- def min_by(value, expression)
30
- keyed(value, expression).min_by(&:first)&.last
34
+ def min_by(value, expression, context:)
35
+ keyed(value, expression, context: context).min_by(&:first)&.last
31
36
  end
32
37
 
33
- def max_by(value, expression)
34
- keyed(value, expression).max_by(&:first)&.last
38
+ def max_by(value, expression, context:)
39
+ keyed(value, expression, context: context).max_by(&:first)&.last
35
40
  end
36
41
 
37
- def keyed(value, expression)
38
- reference = reference(expression)
42
+ def keyed(value, expression, context:)
43
+ reference = reference(expression, context: context, position: 2)
39
44
  # @type var expected: Symbol?
40
45
  expected = nil
41
- array(value).map do |item|
46
+ # @type var result_context: ValidationContext?
47
+ result_context = nil
48
+ array(value, context: context, position: 1).map do |item|
42
49
  key = reference.call(item)
43
- expected = checked_type(key, expected)
44
- [sort_key(key), item]
50
+ type = checked_type(key, expected, context: context)
51
+ expected = type
52
+ key_context = result_context || context.with_types([type])
53
+ result_context = key_context
54
+ [sort_key(key, context: key_context, position: 2, stage: :expression_result), item]
45
55
  end
46
56
  end
47
57
 
48
- def checked_type(value, expected)
58
+ def checked_type(value, expected, context:)
49
59
  type = Types.name(value)
50
60
  unless SORTABLE_TYPES.include?(type)
51
- raise Signature::TypeMismatch.new("Expression keys must be numbers or strings", origin: validation_origin)
61
+ raise context.type_error(
62
+ "Expression keys must be numbers or strings",
63
+ position: 2, expected_types: expected ? [expected] : SORTABLE_TYPES, actual_type: type,
64
+ origin: validation_origin, stage: :expression_result
65
+ )
52
66
  end
53
67
  if expected && expected != type
54
- raise Signature::TypeMismatch.new("Expression keys must have one type",
55
- origin: validation_origin)
68
+ raise context.type_error(
69
+ "Expression keys must have one type",
70
+ position: 2, expected_types: [expected], actual_type: type,
71
+ origin: validation_origin, stage: :expression_result
72
+ )
56
73
  end
57
74
 
58
75
  type
@@ -13,57 +13,74 @@ module JMESPath
13
13
  "to_number" => [[:any]], "not_null" => [[:any]]
14
14
  }.freeze
15
15
 
16
+ DESCRIPTIONS = {
17
+ "abs" => "Absolute value.",
18
+ "ceil" => "Integer rounded upward.",
19
+ "floor" => "Integer rounded downward.",
20
+ "contains" => "Test string or array membership.",
21
+ "ends_with" => "Test whether a string ends with a suffix.",
22
+ "starts_with" => "Test whether a string starts with a prefix.",
23
+ "length" => "Character or element count.",
24
+ "reverse" => "Reverse a string or array.",
25
+ "type" => "JMESPath type name.",
26
+ "to_array" => "Wrap a value in an array, preserving arrays.",
27
+ "to_string" => "String or compact JSON representation.",
28
+ "to_number" => "Number when convertible, otherwise null.",
29
+ "not_null" => "First non-null value."
30
+ }.freeze
31
+
16
32
  def self.register(registry)
17
33
  SIGNATURES.reduce(registry) do |result, (name, signature)|
18
34
  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")
35
+ result.with_builtin(name, signature: signature, handler: handler.to_sym,
36
+ description: DESCRIPTIONS.fetch(name), variadic: name == "not_null")
20
37
  end
21
38
  end
22
39
 
23
40
  private
24
41
 
25
- def abs(value)
26
- number(value).abs
42
+ def abs(value, context:)
43
+ number(value, context: context, position: 1).abs
27
44
  end
28
45
 
29
- def ceil(value)
30
- number(value).ceil
46
+ def ceil(value, context:)
47
+ number(value, context: context, position: 1).ceil
31
48
  end
32
49
 
33
- def floor(value)
34
- number(value).floor
50
+ def floor(value, context:)
51
+ number(value, context: context, position: 1).floor
35
52
  end
36
53
 
37
- def contains?(subject, search)
54
+ def contains?(subject, search, context:)
38
55
  text = DataAccess.string(subject)
39
- return text.include?(string(search)) if text
56
+ return text.include?(string(search, context: context, position: 2)) if text
40
57
 
41
- array(subject).any? { |item| Types.equal?(item, search) }
58
+ array(subject, context: context, position: 1).any? { |item| Types.equal?(item, search) }
42
59
  end
43
60
 
44
- def ends_with?(text, suffix)
45
- string(text).end_with?(string(suffix))
61
+ def ends_with?(text, suffix, context:)
62
+ string(text, context: context, position: 1).end_with?(string(suffix, context: context, position: 2))
46
63
  end
47
64
 
48
- def starts_with?(text, prefix)
49
- string(text).start_with?(string(prefix))
65
+ def starts_with?(text, prefix, context:)
66
+ string(text, context: context, position: 1).start_with?(string(prefix, context: context, position: 2))
50
67
  end
51
68
 
52
- def length(value)
53
- collection = DataAccess.string(value) || DataAccess.array(value) || object(value)
69
+ def length(value, context:)
70
+ collection = DataAccess.string(value) || DataAccess.array(value) || object(value, context: context, position: 1)
54
71
  collection.length
55
72
  end
56
73
 
57
- def reverse(value)
58
- (DataAccess.string(value) || array(value)).reverse
74
+ def reverse(value, context:)
75
+ (DataAccess.string(value) || array(value, context: context, position: 1)).reverse
59
76
  end
60
77
 
61
- def type(value)
78
+ def type(value, **)
62
79
  name = Types.name(value)
63
80
  name.to_s unless name == :unknown
64
81
  end
65
82
 
66
- def not_null(*values)
83
+ def not_null(*values, **)
67
84
  values.find { |value| !value.nil? }
68
85
  end
69
86
  end
@@ -10,9 +10,9 @@ module JMESPath
10
10
  module FailureOrigin
11
11
  attr_reader :origin
12
12
 
13
- def initialize(message = nil, origin: nil)
13
+ def initialize(message = nil, origin: nil, **metadata)
14
14
  @origin = origin
15
- super(message)
15
+ super(message, **metadata)
16
16
  end
17
17
  end
18
18
 
@@ -26,6 +26,12 @@ module JMESPath
26
26
  include FailureOrigin
27
27
  end
28
28
 
29
+ attr_reader :arguments
30
+
31
+ def variadic?
32
+ @variadic
33
+ end
34
+
29
35
  def initialize(arguments, variadic: false)
30
36
  raise ArgumentError, "Invalid function signature" unless valid_definition?(arguments)
31
37
  raise ArgumentError, "Variadic signatures require an argument" if variadic && arguments.empty?
@@ -35,21 +41,29 @@ module JMESPath
35
41
  freeze
36
42
  end
37
43
 
38
- def validate!(values, name, origin: nil)
44
+ def validate!(values, name, origin: nil, context: nil)
45
+ context ||= ValidationContext.new(name, self)
39
46
  unless valid_arity?(values.length)
40
- raise ArityMismatch.new("Invalid number of arguments for #{name}", origin: origin)
47
+ raise context.arity_error("Invalid number of arguments for #{name}", actual_arity: values.length,
48
+ origin: origin)
41
49
  end
42
50
 
43
51
  values.each_with_index do |value, index|
44
52
  types = @arguments.fetch(index) { @arguments.fetch(-1) }
45
53
  next if types.any? { |type| matches?(value, type) }
46
54
 
47
- raise TypeMismatch.new("Invalid argument #{index + 1} for #{name}: #{Types.name(value)}", origin: origin)
55
+ reject_type!(value, index, types, name, origin, context)
48
56
  end
49
57
  end
50
58
 
51
59
  private
52
60
 
61
+ def reject_type!(value, index, types, name, origin, context) # rubocop:disable Metrics/ParameterLists
62
+ actual_type = Types.name(value)
63
+ raise context.type_error("Invalid argument #{index + 1} for #{name}: #{actual_type}",
64
+ position: index + 1, expected_types: types, actual_type: actual_type, origin: origin)
65
+ end
66
+
53
67
  def valid_definition?(arguments)
54
68
  arguments.is_a?(Array) && arguments.all? do |types|
55
69
  types.is_a?(Array) && !types.empty? && (types - TYPES).empty?
@@ -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