enumpath 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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +91 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +56 -0
- data/LICENSE +201 -0
- data/README.md +492 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/enumpath.gemspec +32 -0
- data/lib/enumpath.rb +62 -0
- data/lib/enumpath/logger.rb +76 -0
- data/lib/enumpath/operator.rb +53 -0
- data/lib/enumpath/operator/base.rb +84 -0
- data/lib/enumpath/operator/child.rb +34 -0
- data/lib/enumpath/operator/filter_expression.rb +116 -0
- data/lib/enumpath/operator/recursive_descent.rb +52 -0
- data/lib/enumpath/operator/slice.rb +61 -0
- data/lib/enumpath/operator/subscript_expression.rb +84 -0
- data/lib/enumpath/operator/union.rb +42 -0
- data/lib/enumpath/operator/wildcard.rb +33 -0
- data/lib/enumpath/path.rb +73 -0
- data/lib/enumpath/path/normalized_path.rb +50 -0
- data/lib/enumpath/resolver/property.rb +23 -0
- data/lib/enumpath/resolver/simple.rb +33 -0
- data/lib/enumpath/results.rb +57 -0
- data/lib/enumpath/version.rb +5 -0
- metadata +185 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "enumpath"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/enumpath.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "enumpath/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "enumpath"
|
8
|
+
spec.version = Enumpath::VERSION
|
9
|
+
spec.authors = ["Chris Bloom"]
|
10
|
+
spec.email = ["chrisbloom7@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = "A JSONPath-compatible library for navigating Ruby objects using path expressions"
|
13
|
+
spec.homepage = "https://github.com/youearnedit/enumpath"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# Enumerable#dig was added in Ruby 2.3.0
|
21
|
+
spec.required_ruby_version = '>= 2.3.0'
|
22
|
+
|
23
|
+
spec.add_dependency "to_regexp", "~> 0.2.1"
|
24
|
+
spec.add_dependency "mini_cache", "~> 1.1.0"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
27
|
+
spec.add_development_dependency "null-logger", "~> 0.1"
|
28
|
+
spec.add_development_dependency "pry-byebug", "~> 3.6"
|
29
|
+
spec.add_development_dependency "rake", "~> 12.3"
|
30
|
+
spec.add_development_dependency "rspec-benchmark", "~> 0.3.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.8"
|
32
|
+
end
|
data/lib/enumpath.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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"
|
11
|
+
|
12
|
+
# A JSONPath-compatible library for navigating Ruby objects using path expressions
|
13
|
+
module Enumpath
|
14
|
+
@verbose = false
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Whether verbose mode is enabled. When enabled, the {Enumpath::Logger} will print
|
18
|
+
# information to the logging stream to assist in debugging path expressions.
|
19
|
+
# Defaults to false
|
20
|
+
#
|
21
|
+
# @return [true,false]
|
22
|
+
attr_accessor :verbose
|
23
|
+
|
24
|
+
# Resolve a path expression against an enumerable
|
25
|
+
#
|
26
|
+
# @param path (see Enumpath::Path#initialize)
|
27
|
+
# @param enum (see Enumpath::Path#apply)
|
28
|
+
# @param options [optional, Hash]
|
29
|
+
# @option options [Symbol] :result_type (:value) The type of results to return, `:value` or `:path`
|
30
|
+
# @option options [true, false] :verbose (false) Whether to enable additional output for debugging
|
31
|
+
# @return (see Enumpath::Path#apply)
|
32
|
+
def apply(path, enum, options = {})
|
33
|
+
logger.level = 0
|
34
|
+
@verbose = options.delete(:verbose) || false
|
35
|
+
Enumpath::Path.new(path, result_type: options.delete(:result_type)).apply(enum)
|
36
|
+
end
|
37
|
+
|
38
|
+
# The {Enumpath::Logger} instance to use with verbose mode
|
39
|
+
#
|
40
|
+
# @private
|
41
|
+
# @return [Enumpath::Logger]
|
42
|
+
def logger
|
43
|
+
@logger ||= Enumpath::Logger.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# A shortcut to {Enumpath::logger.log}
|
47
|
+
#
|
48
|
+
# @private
|
49
|
+
# @see Enumpath::Logger#log
|
50
|
+
def log(title)
|
51
|
+
block_given? ? logger.log(title, &Proc.new) : logger.log(title)
|
52
|
+
end
|
53
|
+
|
54
|
+
# A lightweight in-memory cache for caching normalized path expressions
|
55
|
+
#
|
56
|
+
# @private
|
57
|
+
# @return [MiniCache::Store]
|
58
|
+
def path_cache
|
59
|
+
@path_cache ||= MiniCache::Store.new
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Enumpath
|
6
|
+
# A logger for providing debugging information while evaluating path expressions
|
7
|
+
# @private
|
8
|
+
class Logger
|
9
|
+
PAD = " "
|
10
|
+
|
11
|
+
SEPARATOR = '--------------------------------------'
|
12
|
+
|
13
|
+
# @return [Integer] the indentation level to apply to log messages
|
14
|
+
attr_accessor :level
|
15
|
+
|
16
|
+
# @return [::Logger, #<<] a {::Logger}-compatible logger instance
|
17
|
+
attr_accessor :logger
|
18
|
+
|
19
|
+
# @param logdev [String, IO] The log device. See Ruby's {::Logger.new} documentation.
|
20
|
+
def initialize(logdev = STDOUT)
|
21
|
+
@logger = ::Logger.new(logdev)
|
22
|
+
@level = 0
|
23
|
+
@padding = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generates a log message for debugging. Returns fast if {Enumpath.verbose} is false. Accepts an optional block
|
27
|
+
# which must contain a single hash, the contents of which will be added to the log message, and which are lazily
|
28
|
+
# evaluated only if {Enumpath.verbose} is true.
|
29
|
+
#
|
30
|
+
# @param title [String] the title of this log message
|
31
|
+
# @yield A lazily evaluated hash of key/value pairs to include in the log message
|
32
|
+
def log(title)
|
33
|
+
return unless Enumpath.verbose
|
34
|
+
append_log "#{padding}#{SEPARATOR}\n"
|
35
|
+
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
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def append_log(message)
|
50
|
+
logger << message
|
51
|
+
end
|
52
|
+
|
53
|
+
def massaged_value(value)
|
54
|
+
if value.is_a?(Enumerable)
|
55
|
+
enum_for_log(value)
|
56
|
+
elsif value.is_a?(TrueClass)
|
57
|
+
'True'
|
58
|
+
elsif value.is_a?(FalseClass)
|
59
|
+
'False'
|
60
|
+
elsif value.nil?
|
61
|
+
'Nil'
|
62
|
+
else
|
63
|
+
value.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def enum_for_log(enum, length = 50)
|
68
|
+
json = enum.inspect
|
69
|
+
"#{json[0...length]}#{json.length > length ? '...' : ''}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def padding
|
73
|
+
PAD * level.to_i
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'enumpath/operator/base'
|
4
|
+
require 'enumpath/operator/child'
|
5
|
+
require 'enumpath/operator/filter_expression'
|
6
|
+
require 'enumpath/operator/recursive_descent'
|
7
|
+
require 'enumpath/operator/subscript_expression'
|
8
|
+
require 'enumpath/operator/slice'
|
9
|
+
require 'enumpath/operator/union'
|
10
|
+
require 'enumpath/operator/wildcard'
|
11
|
+
|
12
|
+
module Enumpath
|
13
|
+
# Namespace for classes that represent path expression operators
|
14
|
+
module Operator
|
15
|
+
ROOT = '$'
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Infer the type of operator and return an instance of its Enumpath::Operator subclass
|
19
|
+
#
|
20
|
+
# @param operator [String] the operator to infer type on
|
21
|
+
# @param enum [Enumerable] the enumerable to assist in detecting child operators
|
22
|
+
# @return an instance of a subclass of Enumpath::Operator based on what was detected, or nil if nothing was
|
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
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Enumpath
|
4
|
+
module Operator
|
5
|
+
# Abstract base class for Operator definitions. Provides some helper methods for each operator and defines the
|
6
|
+
# basic factory methods that must be implemented.
|
7
|
+
#
|
8
|
+
# @abstract Subclass and override {.detect?} and {#apply} to implement a new path expression operator.
|
9
|
+
class Base
|
10
|
+
class << self
|
11
|
+
# Provides an interface for determining if a given string represents the operator class
|
12
|
+
#
|
13
|
+
# @abstract Override in each path expression operator subclass
|
14
|
+
#
|
15
|
+
# @param operator [String] the the full, normalized operator to test
|
16
|
+
# @param enum [Enumerable] an enum that can be used to assist in detection. Not all subclasses require an enum
|
17
|
+
# for detection.
|
18
|
+
# @return [true, false] whether the operator param appears to represent the operator class
|
19
|
+
def detect?(operator, enum = nil)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String] the full, normalized operator
|
25
|
+
attr_reader :operator
|
26
|
+
|
27
|
+
# Initializes an operator class with an operator string
|
28
|
+
#
|
29
|
+
# @param operator [String] the the full, normalized operator
|
30
|
+
def initialize(operator)
|
31
|
+
@operator = operator
|
32
|
+
end
|
33
|
+
|
34
|
+
# Provides an interface for applying the operator to a given enumerable and yielding that result back to the
|
35
|
+
# caller with updated arguments
|
36
|
+
#
|
37
|
+
# @abstract Override in each path expression operator subclass
|
38
|
+
#
|
39
|
+
# @param remaining_path [Array] an array containing the normalized path segments yet to be resolved
|
40
|
+
# @param enum [Enumerable] the object to apply the operator to
|
41
|
+
# @param resolved_path [Array] an array containing the static path segments that have been resolved
|
42
|
+
#
|
43
|
+
# @yield A block that will be called if the operator is applied successfully. If the operator cannot or should
|
44
|
+
# not be applied then the block is not yielded.
|
45
|
+
#
|
46
|
+
# @yieldparam remaining_path [Array] the new remaining_path after applying the operator
|
47
|
+
# @yieldparam enum [Enumerable] the new enum after applying the operator
|
48
|
+
# @yieldparam resolved_path [Array] the new resolved_path after applying the operator
|
49
|
+
# @yieldreturn [void]
|
50
|
+
def apply(remaining_path, enum, resolved_path, &block)
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
# An alias to {#operator}, used by {Enumpath::Logger}
|
55
|
+
#
|
56
|
+
# @private
|
57
|
+
# @return [String] the operator the class was initialized with
|
58
|
+
def to_s
|
59
|
+
operator
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Returns a set of keys, member names, or indices for a given enumerable. Useful for operators that need to
|
65
|
+
# iterate over each member of an enumerable. If the object is an array then it will return an array of indexes.
|
66
|
+
# If the object can be converted to a hash (Hash, Struct, or anything responding to {#to_h}) then it will be
|
67
|
+
# forced to a hash and the hash keys will be returned. For all other objects, an empty array will be returned.
|
68
|
+
#
|
69
|
+
# @param enum [Enumerable] the object to detect keys, member names, or indices on.
|
70
|
+
# @return [Array] a set of keys, member names, or indices for the object, or an empty set if they could not be
|
71
|
+
# determined.
|
72
|
+
def keys(enum)
|
73
|
+
# Arrays
|
74
|
+
return (0...enum.length).to_a if enum.is_a?(Array)
|
75
|
+
|
76
|
+
# Other Enumerables
|
77
|
+
return enum.to_h.keys if enum.respond_to?(:to_h)
|
78
|
+
|
79
|
+
# Fallback
|
80
|
+
[]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Enumpath
|
4
|
+
module Operator
|
5
|
+
# Implements JSONPath child operator syntax. See {file:README.md#label-Child+operator} for syntax and examples.
|
6
|
+
class Child < Base
|
7
|
+
class << self
|
8
|
+
# Checks to see if an operator is valid as a child operator. It is considered valid if the enumerable contains
|
9
|
+
# an index, key, member, or property that responds to child.
|
10
|
+
#
|
11
|
+
# @param operator (see Enumpath::Operator::Base.detect?)
|
12
|
+
# @return (see Enumpath::Operator::Base.detect?)
|
13
|
+
def detect?(operator, enum)
|
14
|
+
!Enumpath::Resolver::Simple.resolve(operator, enum).nil? ||
|
15
|
+
!Enumpath::Resolver::Property.resolve(operator, enum).nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Resolves a child operator against an enumerable. If the child operator matches a index, key, member, or
|
20
|
+
# property of the enumerable it is yielded to the block.
|
21
|
+
#
|
22
|
+
# @param (see Enumpath::Operator::Base#apply)
|
23
|
+
# @yield (see Enumpath::Operator::Base#apply)
|
24
|
+
# @yieldparam remaining_path [Array] remaining_path
|
25
|
+
# @yieldparam enum [Enumerable] the resolved value of the enumerable
|
26
|
+
# @yieldparam resolved_path [Array] resolved_path plus the child operator
|
27
|
+
def apply(remaining_path, enum, resolved_path, &block)
|
28
|
+
value = Enumpath::Resolver::Simple.resolve(operator, enum)
|
29
|
+
value = Enumpath::Resolver::Property.resolve(operator, enum) if value.nil?
|
30
|
+
yield(remaining_path, value, resolved_path + [operator]) if !value.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'to_regexp'
|
4
|
+
|
5
|
+
module Enumpath
|
6
|
+
module Operator
|
7
|
+
# Implements JSONPath filter expression operator syntax. See {file:README.md#label-Filter+expression+operator} for
|
8
|
+
# syntax and examples
|
9
|
+
class FilterExpression < Base
|
10
|
+
COMPARISON_OPERATOR_REGEX = /(==|!=|>=|<=|<=>|>|<|=~|!~)/
|
11
|
+
LOGICAL_OPERATORS_REGEX = /(&&)|(\|\|)/
|
12
|
+
OPERATOR_REGEX = /^\?\((.*)\)$/
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# Whether the operator matches {Enumpath::Operator::FilterExpression::OPERATOR_REGEX}
|
16
|
+
#
|
17
|
+
# @param operator (see Enumpath::Operator::Base.detect?)
|
18
|
+
# @return (see Enumpath::Operator::Base.detect?)
|
19
|
+
def detect?(operator)
|
20
|
+
!!(operator =~ OPERATOR_REGEX)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Yields to the block once for each member of the enumerable that passes the filter expression
|
25
|
+
#
|
26
|
+
# @param (see Enumpath::Operator::Base#apply)
|
27
|
+
# @yield (see Enumpath::Operator::Base#apply)
|
28
|
+
# @yieldparam remaining_path [Array] remaining_path
|
29
|
+
# @yieldparam enum [Enumerable] the member of the enumerable that passed the filter
|
30
|
+
# @yieldparam resolved_path [Array] resolved_path plus the key for each member of the enumerable that passed
|
31
|
+
# the filter
|
32
|
+
def apply(remaining_path, enum, resolved_path, &block)
|
33
|
+
Enumpath.log('Evaluating filter expression') { { expression: operator, to: enum } }
|
34
|
+
|
35
|
+
_match, unpacked_operator = OPERATOR_REGEX.match(operator).to_a
|
36
|
+
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
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def pass?(expressions, enum)
|
51
|
+
running_result = evaluate(expressions.shift, enum)
|
52
|
+
Enumpath.log('Initial result') { { result: running_result } }
|
53
|
+
while expressions.any? do
|
54
|
+
logical_operator, expression = expressions.shift(2)
|
55
|
+
running_result = evaluate(expression, enum, logical_operator, running_result)
|
56
|
+
Enumpath.log('Running result') { { result: running_result } }
|
57
|
+
end
|
58
|
+
running_result
|
59
|
+
end
|
60
|
+
|
61
|
+
def evaluate(expression, enum, logical_operator = nil, running_result = nil)
|
62
|
+
property, operator, operand = expression.split(COMPARISON_OPERATOR_REGEX).map(&:strip)
|
63
|
+
value = resolve(property, enum)
|
64
|
+
expression_result = test(operator, operand, value)
|
65
|
+
Enumpath.log('Evaluated filter') do
|
66
|
+
{ property => value, operator: operator, operand: operand, result: expression_result,
|
67
|
+
logical_operator: logical_operator }.compact
|
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
|
78
|
+
end
|
79
|
+
|
80
|
+
def resolve(property, enum)
|
81
|
+
return enum if property == '@'
|
82
|
+
value = Enumpath::Resolver::Simple.resolve(property.gsub(/^@\./, ''), enum)
|
83
|
+
value = Enumpath::Resolver::Property.resolve(property.gsub(/^@\./, ''), enum) if value.nil?
|
84
|
+
value
|
85
|
+
end
|
86
|
+
|
87
|
+
def test(operator, operand, value)
|
88
|
+
return !!value if operator.nil? || operand.nil?
|
89
|
+
typecast_operand = variable_typecaster(operand)
|
90
|
+
!!value.public_send(operator.to_sym, typecast_operand)
|
91
|
+
rescue NoMethodError
|
92
|
+
Enumpath.log('Filter could not be evaluated!')
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
def variable_typecaster(variable)
|
97
|
+
if variable =~ /\A('|").+\1\z/
|
98
|
+
# It quacks like a string
|
99
|
+
variable.gsub(/\A('|")|('|")\z/, '')
|
100
|
+
elsif variable =~ /^:.+/
|
101
|
+
# It quacks like a symbol
|
102
|
+
variable.gsub(/\A:/, '').to_sym
|
103
|
+
elsif variable =~ /true|false|nil/i
|
104
|
+
# It quacks like an unquoted boolean operator
|
105
|
+
variable == 'true' ? true : false
|
106
|
+
elsif regexp = variable.to_regexp(literal: false, detect: false)
|
107
|
+
# It quacks like a regex
|
108
|
+
regexp
|
109
|
+
else
|
110
|
+
# Otherwise treat it as a number
|
111
|
+
variable.to_f
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|