Class: Path::Matcher

Inherits:
Object
  • Object
show all
Defined in:
lib/path/matcher.rb

Overview

Path::Matcher is representation of a single node of a relative path used to find values in a data set.

Defined Under Namespace

Modules: ANY_VALUE

Constant Summary collapse

SUFF_CHARS =

Shortcut characters that require modification before being turned into a matcher.

Regexp.escape "*?"
PATH_CHARS =

All special path characters.

Regexp.escape("()|") << SUFF_CHARS
RESC_CHARS =

Path chars that get regexp escaped.

"*?()|/"
RANGE_MATCHER =

Matcher for Range path item.

%r{^(\-?\d+)(\.{2,3})(\-?\d+)$}
ILEN_MATCHER =

Matcher for index,length path item.

%r{^(\-?\d+),(\-?\d+)$}
ANYVAL_MATCHER =

Matcher allowing any value to be matched.

/^(\?*\*+\?*)*$/
PATH_CHAR_MATCHER =

Matcher to assert if any unescaped special chars are in a path item.

/(^|[^#{Path::RECH}])([#{PATH_CHARS}])/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Matcher

New instance of Matcher. Options supported:

:key

String - The path item key to match.

:value

String - The path item value to match.

:recursive

Boolean - Look for path item recursively.

:regex_opts

Fixnum - representing the Regexp options.



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/path/matcher.rb', line 43

def initialize opts={}
  @regex_opts = opts[:regex_opts]
  @recursive  = !!opts[:recursive]

  @key = nil
  @key = parse_node opts[:key] if
    opts[:key] && !opts[:key].to_s.empty?

  @value = nil
  @value = parse_node opts[:value] if
    opts[:value] && !opts[:value].to_s.empty?
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



33
34
35
# File 'lib/path/matcher.rb', line 33

def key
  @key
end

#recursiveObject

Returns the value of attribute recursive.



34
35
36
# File 'lib/path/matcher.rb', line 34

def recursive
  @recursive
end

#regex_optsObject (readonly)

Returns the value of attribute regex_opts.



33
34
35
# File 'lib/path/matcher.rb', line 33

def regex_opts
  @regex_opts
end

#valueObject (readonly)

Returns the value of attribute value.



33
34
35
# File 'lib/path/matcher.rb', line 33

def value
  @value
end

Instance Method Details

#==(other) ⇒ Object

:nodoc:



57
58
59
60
61
62
# File 'lib/path/matcher.rb', line 57

def == other # :nodoc:
  self.class  == other.class      &&
  @key        == other.key        &&
  @value      == other.value      &&
  @regex_opts == other.regex_opts
end

#each_data_item(data, &block) ⇒ Object

Universal iterator for Hash and Array like objects. The data argument must either respond to both :each_with_index and :length, or respond to :has_key? and :each yielding a key/value pair.



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/path/matcher.rb', line 70

def each_data_item data, &block
  if data.respond_to?(:has_key?) && data.respond_to?(:each)
    data.each(&block)

  elsif data.respond_to?(:each_with_index) && data.respond_to?(:length)
    # We need to iterate through the array this way
    # in case items in it get deleted.
    (data.length - 1).downto(0) do |i|
      block.call i, data[i]
    end
  end
end

#find_in(data, path = nil, &block) ⇒ Object

Finds data with the given key and value matcher, optionally recursive. Yields data, key and path Array when block is given. Returns an Array of path arrays.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/path/matcher.rb', line 89

def find_in data, path=nil, &block
  return [] unless Array === data || Hash === data

  paths  = []
  path ||= Path::Match.new
  path   = Path::Match.new path if path.class == Array

  each_data_item data do |key, value|
    c_path = path.dup << key

    found, kmatch = match_node(@key, key)     if @key
    found, vmatch = match_node(@value, value) if @value && (!@key || found)

    c_path.append_splat self, key if @recursive

    if found
      c_path.matches.concat kmatch.to_a
      c_path.matches.concat vmatch.to_a

      f_path = c_path.dup
      f_path.splat[-1][-1].pop if @key && !f_path.splat.empty?

      yield data, key, f_path if block_given?
      paths << f_path
    end

    paths.concat find_in(data[key], c_path, &block) if @recursive
  end

  paths
end

#match_node(node, value) ⇒ Object

Check if data key or value is a match for nested data searches. Returns an array with a boolean expressing if the value matched the node, and the matches found.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/path/matcher.rb', line 127

def match_node node, value
  return if ANY_VALUE != node &&
            (Array === value || Hash === value)

  if node.class == value.class
    node == value

  elsif Regexp === node
    match = node.match value.to_s
    return false unless match
    match = match.size > 1 ? match[1..-1] : match.to_a
    [true, match]

  elsif Range === node
    stat  = node.include? value.to_i
    match = [value.to_i] if stat
    [stat, match]

  elsif ANY_VALUE == node
    [true, [value]]

  else
    value.to_s == node.to_s
  end
end

#parse_node(str) ⇒ Object

Decide whether to make path item matcher a regex, range, array, or string.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/path/matcher.rb', line 157

def parse_node str
  case str
  when nil, ANYVAL_MATCHER
    ANY_VALUE

  when RANGE_MATCHER
    Range.new $1.to_i, $3.to_i, ($2 == "...")

  when ILEN_MATCHER
    Range.new $1.to_i, ($1.to_i + $2.to_i), true

  when String
    if @regex_opts || str =~ PATH_CHAR_MATCHER

      # Remove extra suffix characters
      str.gsub! %r{(^|[^#{Path::RECH}])(\*+\?*)}, '\1*'
      str.gsub! %r{(^|[^#{Path::RECH}])\*+}, '\1*'

      str = Regexp.escape str

      # Remove escaping from special path characters
      str.gsub! %r{#{Path::RECH}([#{PATH_CHARS}])}, '\1'
      str.gsub! %r{#{Path::RECH}([#{RESC_CHARS}])}, '\1'
      str.gsub! %r{(^|[^#{Path::RECH}])([#{SUFF_CHARS}])},   '\1(.\2)'
      str.gsub! %r{(^|[^\.#{Path::RECH}])([#{SUFF_CHARS}])}, '\1(.\2)'

      Regexp.new "\\A(?:#{str})\\Z", @regex_opts

    else
      str.gsub %r{#{Path::RECH}([^#{Path::RECH}]|$)}, '\1'
    end

  else
    str
  end
end