enumpath 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|