enumpath 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "enumpath"
4
+ require 'bundler/setup'
5
+ require 'enumpath'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "enumpath"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
data/enumpath.gemspec CHANGED
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'enumpath/version'
5
6
 
@@ -8,7 +9,7 @@ Gem::Specification.new do |spec|
8
9
  spec.version = Enumpath::VERSION
9
10
  spec.license = 'Apache-2.0'
10
11
  spec.summary = 'A JSONPath-compatible library for navigating nested Ruby objects using path expressions'
11
- spec.description = <<~EOF
12
+ spec.description = <<~DESC
12
13
  Enumpath is an implementation of the JSONPath spec for Ruby objects,
13
14
  plus some added sugar. It's like Ruby's native Enumerable#dig method,
14
15
  but fancier. It is designed for situations where you need to provide
@@ -16,27 +17,29 @@ Gem::Specification.new do |spec|
16
17
  objects. This makes it exceptionally well suited for flexible ETL
17
18
  (Extract, Transform, Load) processes by allowing you to define paths
18
19
  through your data in a simple, easily readable, easily storable syntax.
19
- EOF
20
+ DESC
20
21
  spec.authors = ['Chris Bloom']
21
- spec.email = ['open-source@youearnedit.com', 'chrisbloom7@gmail.com']
22
+ spec.email = ['chrisbloom7@gmail.com']
22
23
 
23
24
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
25
  f.match(%r{^(test|spec|features)/})
25
26
  end
26
27
  spec.require_paths = ['lib']
27
28
 
28
- spec.homepage = 'https://github.com/youearnedit/enumpath'
29
+ spec.homepage = 'https://github.com/chrisbloom7/enumpath'
29
30
 
30
31
  # Enumerable#dig was added in Ruby 2.3.0
31
32
  spec.required_ruby_version = '>= 2.3.0'
32
33
 
33
- spec.add_dependency 'to_regexp', '~> 0.2.1'
34
34
  spec.add_dependency 'mini_cache', '~> 1.1.0'
35
+ spec.add_dependency 'to_regexp', '~> 0.2.1'
35
36
 
36
- spec.add_development_dependency 'bundler', '~> 1.16'
37
+ spec.add_development_dependency 'bundler', '~> 2.1'
37
38
  spec.add_development_dependency 'null-logger', '~> 0.1'
38
39
  spec.add_development_dependency 'pry-byebug', '~> 3.6'
39
40
  spec.add_development_dependency 'rake', '~> 12.3'
40
- spec.add_development_dependency 'rspec-benchmark', '~> 0.3.0'
41
41
  spec.add_development_dependency 'rspec', '~> 3.8'
42
+ spec.add_development_dependency 'rspec-benchmark', '~> 0.3.0'
43
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
44
+ spec.add_development_dependency 'yard', '~> 0.9.26'
42
45
  end
data/lib/enumpath.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'mini_cache'
4
- require "enumpath/logger"
5
- require "enumpath/operator"
6
- require "enumpath/path"
7
- require "enumpath/results"
8
- require "enumpath/resolver/simple"
9
- require "enumpath/resolver/property"
10
- require "enumpath/version"
4
+ require 'enumpath/logger'
5
+ require 'enumpath/operator'
6
+ require 'enumpath/path'
7
+ require 'enumpath/results'
8
+ require 'enumpath/resolver/simple'
9
+ require 'enumpath/resolver/property'
10
+ require 'enumpath/version'
11
11
 
12
12
  # A JSONPath-compatible library for navigating Ruby objects using path expressions
13
13
  module Enumpath
@@ -48,7 +48,7 @@ module Enumpath
48
48
  # @private
49
49
  # @see Enumpath::Logger#log
50
50
  def log(title)
51
- block_given? ? logger.log(title, &Proc.new) : logger.log(title)
51
+ block_given? ? logger.log(title, &-> { yield }) : logger.log(title)
52
52
  end
53
53
 
54
54
  # A lightweight in-memory cache for caching normalized path expressions
@@ -6,7 +6,7 @@ module Enumpath
6
6
  # A logger for providing debugging information while evaluating path expressions
7
7
  # @private
8
8
  class Logger
9
- PAD = " "
9
+ PAD = ' '
10
10
 
11
11
  SEPARATOR = '--------------------------------------'
12
12
 
@@ -31,17 +31,11 @@ module Enumpath
31
31
  # @yield A lazily evaluated hash of key/value pairs to include in the log message
32
32
  def log(title)
33
33
  return unless Enumpath.verbose
34
+
34
35
  append_log "#{padding}#{SEPARATOR}\n"
35
36
  append_log "#{padding}Enumpath: #{title}\n"
36
- if block_given?
37
- append_log "#{padding}#{SEPARATOR}\n"
38
- vars = yield
39
- return unless vars.is_a?(Hash)
40
- label_size = vars.keys.map(&:size).max
41
- vars.each do |label, value|
42
- append_log "#{padding}#{label.to_s.ljust(label_size)}: #{massaged_value(value)}\n"
43
- end
44
- end
37
+ append_log "#{padding}#{SEPARATOR}\n" if block_given?
38
+ log_vars(yield) if block_given?
45
39
  end
46
40
 
47
41
  private
@@ -50,7 +44,16 @@ module Enumpath
50
44
  logger << message
51
45
  end
52
46
 
53
- def massaged_value(value)
47
+ def log_vars(vars)
48
+ return unless vars.is_a?(Hash)
49
+
50
+ label_size = vars.keys.map(&:size).max
51
+ vars.each do |label, value|
52
+ append_log "#{padding}#{label.to_s.ljust(label_size)}: #{massaged_value(value)}\n"
53
+ end
54
+ end
55
+
56
+ def massaged_value(value) # rubocop:disable Metrics/MethodLength
54
57
  if value.is_a?(Enumerable)
55
58
  enum_for_log(value)
56
59
  elsif value.is_a?(TrueClass)
@@ -21,32 +21,53 @@ module Enumpath
21
21
  # @param enum [Enumerable] the enumerable to assist in detecting child operators
22
22
  # @return an instance of a subclass of Enumpath::Operator based on what was detected, or nil if nothing was
23
23
  # detected
24
- def detect(operator, enum)
25
- if Enumpath::Operator::Child.detect?(operator, enum)
26
- Enumpath.log('Child operator detected')
27
- Enumpath::Operator::Child.new(operator)
28
- elsif Enumpath::Operator::Wildcard.detect?(operator)
29
- Enumpath.log('Wildcard operator detected')
30
- Enumpath::Operator::Wildcard.new(operator)
31
- elsif Enumpath::Operator::RecursiveDescent.detect?(operator)
32
- Enumpath.log('Recursive Descent operator detected')
33
- Enumpath::Operator::RecursiveDescent.new(operator)
34
- elsif Enumpath::Operator::Union.detect?(operator)
35
- Enumpath.log('Union operator detected')
36
- Enumpath::Operator::Union.new(operator)
37
- elsif Enumpath::Operator::SubscriptExpression.detect?(operator)
38
- Enumpath.log('Subscript Expression operator detected')
39
- Enumpath::Operator::SubscriptExpression.new(operator)
40
- elsif Enumpath::Operator::FilterExpression.detect?(operator)
41
- Enumpath.log('Filter Expression operator detected')
42
- Enumpath::Operator::FilterExpression.new(operator)
43
- elsif Enumpath::Operator::Slice.detect?(operator)
44
- Enumpath.log('Slice operator detected')
45
- Enumpath::Operator::Slice.new(operator)
46
- else
47
- Enumpath.log('Not a valid operator for enum')
48
- nil
49
- end
24
+ def detect(operator, enum) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
25
+ return operator(:Child, operator) if child?(operator, enum)
26
+ return operator(:Wildcard, operator) if wildcard?(operator)
27
+ return operator(:RecursiveDescent, operator) if recursive_descent?(operator)
28
+ return operator(:Union, operator) if union?(operator)
29
+ return operator(:SubscriptExpression, operator) if subscript_expression?(operator)
30
+ return operator(:FilterExpression, operator) if filter_expression?(operator)
31
+ return operator(:Slice, operator) if slice?(operator)
32
+
33
+ Enumpath.log('Not a valid operator for enum')
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def child?(operator, enum)
40
+ Enumpath::Operator::Child.detect?(operator, enum)
41
+ end
42
+
43
+ def wildcard?(operator)
44
+ Enumpath::Operator::Wildcard.detect?(operator)
45
+ end
46
+
47
+ def recursive_descent?(operator)
48
+ Enumpath::Operator::RecursiveDescent.detect?(operator)
49
+ end
50
+
51
+ def union?(operator)
52
+ Enumpath::Operator::Union.detect?(operator)
53
+ end
54
+
55
+ def subscript_expression?(operator)
56
+ Enumpath::Operator::SubscriptExpression.detect?(operator)
57
+ end
58
+
59
+ def filter_expression?(operator)
60
+ Enumpath::Operator::FilterExpression.detect?(operator)
61
+ end
62
+
63
+ def slice?(operator)
64
+ Enumpath::Operator::Slice.detect?(operator)
65
+ end
66
+
67
+ def operator(operator_class, operator)
68
+ Enumpath.log("#{operator_class} operator detected")
69
+ klass = Object.const_get("Enumpath::Operator::#{operator_class}")
70
+ klass.new(operator)
50
71
  end
51
72
  end
52
73
  end
@@ -16,7 +16,7 @@ module Enumpath
16
16
  # @param enum [Enumerable] an enum that can be used to assist in detection. Not all subclasses require an enum
17
17
  # for detection.
18
18
  # @return [true, false] whether the operator param appears to represent the operator class
19
- def detect?(operator, enum = nil)
19
+ def detect?(_operator, _enum = nil)
20
20
  raise NotImplementedError
21
21
  end
22
22
  end
@@ -47,7 +47,7 @@ module Enumpath
47
47
  # @yieldparam enum [Enumerable] the new enum after applying the operator
48
48
  # @yieldparam resolved_path [Array] the new resolved_path after applying the operator
49
49
  # @yieldreturn [void]
50
- def apply(remaining_path, enum, resolved_path, &block)
50
+ def apply(_remaining_path, _enum, _resolved_path)
51
51
  raise NotImplementedError
52
52
  end
53
53
 
@@ -24,10 +24,10 @@ module Enumpath
24
24
  # @yieldparam remaining_path [Array] remaining_path
25
25
  # @yieldparam enum [Enumerable] the resolved value of the enumerable
26
26
  # @yieldparam resolved_path [Array] resolved_path plus the child operator
27
- def apply(remaining_path, enum, resolved_path, &block)
27
+ def apply(remaining_path, enum, resolved_path)
28
28
  value = Enumpath::Resolver::Simple.resolve(operator, enum)
29
29
  value = Enumpath::Resolver::Property.resolve(operator, enum) if value.nil?
30
- yield(remaining_path, value, resolved_path + [operator]) if !value.nil?
30
+ yield(remaining_path, value, resolved_path + [operator]) unless value.nil?
31
31
  end
32
32
  end
33
33
  end
@@ -17,7 +17,7 @@ module Enumpath
17
17
  # @param operator (see Enumpath::Operator::Base.detect?)
18
18
  # @return (see Enumpath::Operator::Base.detect?)
19
19
  def detect?(operator)
20
- !!(operator =~ OPERATOR_REGEX)
20
+ !(operator =~ OPERATOR_REGEX).nil?
21
21
  end
22
22
  end
23
23
 
@@ -31,26 +31,26 @@ module Enumpath
31
31
  # the filter
32
32
  def apply(remaining_path, enum, resolved_path, &block)
33
33
  Enumpath.log('Evaluating filter expression') { { expression: operator, to: enum } }
34
-
35
34
  _match, unpacked_operator = OPERATOR_REGEX.match(operator).to_a
36
35
  expressions = unpacked_operator.split(LOGICAL_OPERATORS_REGEX).map(&:strip)
37
-
38
- keys(enum).each do |key|
39
- value = Enumpath::Resolver::Simple.resolve(key, enum)
40
- Enumpath.log('Applying filter to key') { { key: key, enum: value } }
41
- if pass?(expressions.dup, value)
42
- Enumpath.log('Applying filtered key') { { 'filtered key': key, 'filtered enum': value } }
43
- yield(remaining_path, value, resolved_path + [key.to_s])
44
- end
45
- end
36
+ keys(enum).each { |key| apply_to_key(key, expressions, remaining_path, enum, resolved_path, &block) }
46
37
  end
47
38
 
48
39
  private
49
40
 
41
+ def apply_to_key(key, expressions, remaining_path, enum, resolved_path)
42
+ value = Enumpath::Resolver::Simple.resolve(key, enum)
43
+ Enumpath.log('Applying filter to key') { { key: key, enum: value } }
44
+ return unless pass?(expressions.dup, value)
45
+
46
+ Enumpath.log('Applying filtered key') { { 'filtered key': key, 'filtered enum': value } }
47
+ yield(remaining_path, value, resolved_path + [key.to_s])
48
+ end
49
+
50
50
  def pass?(expressions, enum)
51
51
  running_result = evaluate(expressions.shift, enum)
52
52
  Enumpath.log('Initial result') { { result: running_result } }
53
- while expressions.any? do
53
+ while expressions.any?
54
54
  logical_operator, expression = expressions.shift(2)
55
55
  running_result = evaluate(expression, enum, logical_operator, running_result)
56
56
  Enumpath.log('Running result') { { result: running_result } }
@@ -66,34 +66,28 @@ module Enumpath
66
66
  { property => value, operator: operator, operand: operand, result: expression_result,
67
67
  logical_operator: logical_operator }.compact
68
68
  end
69
- if logical_operator == '&&'
70
- Enumpath.log('&&=')
71
- running_result &&= expression_result
72
- elsif logical_operator == '||'
73
- Enumpath.log('||=')
74
- running_result ||= expression_result
75
- else
76
- expression_result
77
- end
69
+ running_result(logical_operator, running_result, expression_result)
78
70
  end
79
71
 
80
72
  def resolve(property, enum)
81
73
  return enum if property == '@'
74
+
82
75
  value = Enumpath::Resolver::Simple.resolve(property.gsub(/^@\./, ''), enum)
83
76
  value = Enumpath::Resolver::Property.resolve(property.gsub(/^@\./, ''), enum) if value.nil?
84
77
  value
85
78
  end
86
79
 
87
80
  def test(operator, operand, value)
88
- return !!value if operator.nil? || operand.nil?
81
+ return (value ? true : false) if operator.nil? || operand.nil?
82
+
89
83
  typecast_operand = variable_typecaster(operand)
90
- !!value.public_send(operator.to_sym, typecast_operand)
84
+ value.public_send(operator.to_sym, typecast_operand) ? true : false
91
85
  rescue NoMethodError
92
86
  Enumpath.log('Filter could not be evaluated!')
93
87
  false
94
88
  end
95
89
 
96
- def variable_typecaster(variable)
90
+ def variable_typecaster(variable) # rubocop:disable Metrics/MethodLength
97
91
  if variable =~ /\A('|").+\1\z/
98
92
  # It quacks like a string
99
93
  variable.gsub(/\A('|")|('|")\z/, '')
@@ -102,8 +96,8 @@ module Enumpath
102
96
  variable.gsub(/\A:/, '').to_sym
103
97
  elsif variable =~ /true|false|nil/i
104
98
  # It quacks like an unquoted boolean operator
105
- variable == 'true' ? true : false
106
- elsif regexp = variable.to_regexp(literal: false, detect: false)
99
+ variable == 'true'
100
+ elsif (regexp = variable.to_regexp(literal: false, detect: false))
107
101
  # It quacks like a regex
108
102
  regexp
109
103
  else
@@ -111,6 +105,18 @@ module Enumpath
111
105
  variable.to_f
112
106
  end
113
107
  end
108
+
109
+ def running_result(logical_operator, running_result, expression_result)
110
+ if logical_operator == '&&'
111
+ Enumpath.log('&&=')
112
+ running_result && expression_result
113
+ elsif logical_operator == '||'
114
+ Enumpath.log('||=')
115
+ running_result || expression_result
116
+ else
117
+ expression_result
118
+ end
119
+ end
114
120
  end
115
121
  end
116
122
  end
@@ -13,7 +13,7 @@ module Enumpath
13
13
  # @param operator (see Enumpath::Operator::Base.detect?)
14
14
  # @return (see Enumpath::Operator::Base.detect?)
15
15
  def detect?(operator)
16
- !!(operator == OPERATOR)
16
+ operator == OPERATOR
17
17
  end
18
18
  end
19
19
 
@@ -28,17 +28,17 @@ module Enumpath
28
28
  # enumerable member
29
29
  # @yieldparam resolved_path [Array] resolved_path for the enumerable itself, or resolved_path plus the key for
30
30
  # each direct enumerable member
31
- def apply(remaining_path, enum, resolved_path, &block)
31
+ def apply(remaining_path, enum, resolved_path)
32
32
  Enumpath.log('Applying remaining path recursively to enum') { { 'remaining path': remaining_path } }
33
33
  yield(remaining_path, enum, resolved_path)
34
34
  keys(enum).each do |key|
35
35
  value = Enumpath::Resolver::Simple.resolve(key, enum)
36
- if recursable?(value)
37
- Enumpath.log('Applying remaining path recursively to key') do
38
- { key: key, 'remaining path': ['..'] + remaining_path }
39
- end
40
- yield(['..'] + remaining_path, value, resolved_path + [key])
36
+ next unless recursable?(value)
37
+
38
+ Enumpath.log('Applying remaining path recursively to key') do
39
+ { key: key, 'remaining path': ['..'] + remaining_path }
41
40
  end
41
+ yield(['..'] + remaining_path, value, resolved_path + [key])
42
42
  end
43
43
  end
44
44
 
@@ -12,7 +12,7 @@ module Enumpath
12
12
  # @param operator (see Enumpath::Operator::Base.detect?)
13
13
  # @return (see Enumpath::Operator::Base.detect?)
14
14
  def detect?(operator)
15
- !!(operator =~ OPERATOR_REGEX)
15
+ !(operator =~ OPERATOR_REGEX).nil?
16
16
  end
17
17
  end
18
18
 
@@ -25,7 +25,7 @@ module Enumpath
25
25
  # @yieldparam remaining_path [Array] the included index plus remaining_path
26
26
  # @yieldparam enum [Enumerable] enum
27
27
  # @yieldparam resolved_path [Array] resolved_path
28
- def apply(remaining_path, enum, resolved_path, &block)
28
+ def apply(remaining_path, enum, resolved_path)
29
29
  _match, start, length, step = OPERATOR_REGEX.match(operator).to_a
30
30
  max_length = enum.size
31
31
  slices(start, length, step, max_length).each do |index|
@@ -5,7 +5,7 @@ module Enumpath
5
5
  # Implements JSONPath subscript expressions operator syntax. See
6
6
  # {file:README.md#label-Subscript+expressions+operator} for syntax and examples
7
7
  class SubscriptExpression < Base
8
- ARITHMETIC_OPERATOR_REGEX = /(\+|-|\*\*|\*|\/|%)/
8
+ ARITHMETIC_OPERATOR_REGEX = %r{(\+|-|\*\*|\*|\/|%)}
9
9
  OPERATOR_REGEX = /^\((.*)\)$/
10
10
 
11
11
  class << self
@@ -14,7 +14,7 @@ module Enumpath
14
14
  # @param operator (see Enumpath::Operator::Base.detect?)
15
15
  # @return (see Enumpath::Operator::Base.detect?)
16
16
  def detect?(operator)
17
- !!(operator =~ OPERATOR_REGEX)
17
+ !(operator =~ OPERATOR_REGEX).nil?
18
18
  end
19
19
  end
20
20
 
@@ -25,17 +25,15 @@ module Enumpath
25
25
  # @yieldparam remaining_path [Array] remaining_path
26
26
  # @yieldparam enum [Enumerable] the member of the enumerable at the value of the subscript expression
27
27
  # @yieldparam resolved_path [Array] resolved_path plus the value of the subscript expression
28
- def apply(remaining_path, enum, resolved_path, &block)
28
+ def apply(remaining_path, enum, resolved_path)
29
29
  Enumpath.log('Applying subscript expression') { { expression: operator, to: enum } }
30
30
 
31
31
  _match, unpacked_operator = OPERATOR_REGEX.match(operator).to_a
32
32
  result = evaluate(unpacked_operator, enum)
33
33
 
34
34
  value = Enumpath::Resolver::Simple.resolve(result, enum)
35
- if !value.nil?
36
- Enumpath.log('Applying subscript') { { 'enum at subscript': value } }
37
- yield(remaining_path, value, resolved_path + [result.to_s])
38
- end
35
+ Enumpath.log('Applying subscript') { { 'enum at subscript': value } } unless value.nil?
36
+ yield(remaining_path, value, resolved_path + [result.to_s]) unless value.nil?
39
37
  end
40
38
 
41
39
  private
@@ -47,6 +45,7 @@ module Enumpath
47
45
 
48
46
  def resolve(property, enum)
49
47
  return enum if property == '@'
48
+
50
49
  value = Enumpath::Resolver::Simple.resolve(property.gsub(/^@\./, ''), enum)
51
50
  value = Enumpath::Resolver::Property.resolve(property.gsub(/^@\./, ''), enum) if value.nil?
52
51
  value
@@ -57,19 +56,22 @@ module Enumpath
57
56
  Enumpath.log('Simple subscript') { { subscript: value } }
58
57
  value
59
58
  else
60
- # Evaluate expression using operator
61
- typecast_operand = variable_typecaster(operand)
62
- result = value.public_send(operator.to_sym, typecast_operand)
63
- Enumpath.log('Evaluated subscript') do
64
- { value: value, operator: operator, operand: typecast_operand, result: result }
65
- end
66
- result
59
+ evaluate_with_operator(operator, operand, value)
67
60
  end
68
61
  rescue NoMethodError
69
62
  Enumpath.log('Subscript could not be evaluated') { { subscript: nil } }
70
63
  nil
71
64
  end
72
65
 
66
+ def evaluate_with_operator(operator, operand, value)
67
+ typecast_operand = variable_typecaster(operand)
68
+ result = value.public_send(operator.to_sym, typecast_operand)
69
+ Enumpath.log('Evaluated subscript') do
70
+ { value: value, operator: operator, operand: typecast_operand, result: result }
71
+ end
72
+ result
73
+ end
74
+
73
75
  def variable_typecaster(variable)
74
76
  if variable =~ /\A('|").+\1\z/ || variable =~ /^:.+/
75
77
  # It quacks like a string or symbol