enumpath 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.
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