Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Use the new treetop based query parser from the chef-solo-search

repository. This adds a wide range of extra query possibilities
to the search patch, like grouping, '?' and '*' wildcards and
much more.
  • Loading branch information...
commit 161a3e481c7bd450cdc2f2fa8431e93afd5253d4 1 parent 8669525
@thekorn thekorn authored
View
1  MANIFEST.in
@@ -4,3 +4,4 @@ include NOTICE
include littlechef/data_bags.rb
include littlechef/search.rb
include littlechef/solo.rb
+include littlechef/parser.rb
View
7 littlechef/chef.py
@@ -224,10 +224,9 @@ def _add_data_bag_patch():
with hide('running', 'stdout'):
sudo('mkdir -p {0}'.format(lib_path))
# Create remote data bags patch
- put(os.path.join(basedir, 'data_bags.rb'),
- os.path.join(lib_path, 'data_bags.rb'), use_sudo=True)
- put(os.path.join(basedir, 'search.rb'),
- os.path.join(lib_path, 'search.rb'), use_sudo=True)
+ for filename in ('data_bags.rb', 'search.rb', 'parser.rb'):
+ put(os.path.join(basedir, filename),
+ os.path.join(lib_path, filename), use_sudo=True)
def _configure_node(configfile):
View
200 littlechef/parser.rb
@@ -0,0 +1,200 @@
+#
+# Copyright 2011, edelight GmbH
+#
+# Authors:
+# Markus Korn <markus.korn@edelight.de>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'treetop'
+require 'chef/solr_query/query_transform'
+
+# mock QueryTransform such that we can access the location of the lucene grammar
+class Chef
+ class SolrQuery
+ class QueryTransform
+ def self.base_path
+ class_variable_get(:@@base_path)
+ end
+ end
+ end
+end
+
+module Lucene
+
+ class Term < Treetop::Runtime::SyntaxNode
+ # compares a query value and a value, tailing '*'-wildcards are handled correctly.
+ # Value can either be a string or an array, all other objects are converted
+ # to a string and than checked.
+ def match( value )
+ if value.is_a?(Array)
+ value.any?{ |x| self.match(x) }
+ else
+ File.fnmatch(self.text_value, value.to_s)
+ end
+ end
+ end
+
+ class Field < Treetop::Runtime::SyntaxNode
+ # simple field -> value matches, supporting tailing '*'-wildcards in keys
+ # as well as in values
+ def match( item )
+ if self.elements[0].text_value == "chef_environment"
+ raise "searching by the chef_environment node attribute is not supported, use a custom 'environment' attribute instead"
+ else
+ keys = self.elements[0].match(item)
+ if keys.nil?
+ false
+ else
+ keys.any?{ |key| self.elements[1].match(item[key]) }
+ end
+ end
+ end
+ end
+
+ # we don't support range matches
+ # range of integers would be easy to implement
+ # but string ranges are hard
+ class FiledRange < Treetop::Runtime::SyntaxNode
+ end
+
+ class InclFieldRange < FieldRange
+ end
+
+ class ExclFieldRange < FieldRange
+ end
+
+ class RangeValue < Treetop::Runtime::SyntaxNode
+ end
+
+ class FieldName < Treetop::Runtime::SyntaxNode
+ def match( item )
+ if self.text_value.end_with?("*")
+ part = self.text_value.chomp("*")
+ item.keys.collect{ |key| key.start_with?(part)? key: nil}.compact
+ else
+ if item.has_key?(self.text_value)
+ [self.text_value,]
+ else
+ nil
+ end
+ end
+ end
+ end
+
+ class Body < Treetop::Runtime::SyntaxNode
+ def match( item )
+ self.elements[0].match( item )
+ end
+ end
+
+ class Group < Treetop::Runtime::SyntaxNode
+ def match( item )
+ self.elements[0].match(item)
+ end
+ end
+
+ class BinaryOp < Treetop::Runtime::SyntaxNode
+ def match( item )
+ self.elements[1].match(
+ self.elements[0].match(item),
+ self.elements[2].match(item)
+ )
+ end
+ end
+
+ class OrOperator < Treetop::Runtime::SyntaxNode
+ def match( cond1, cond2 )
+ cond1 or cond2
+ end
+ end
+
+ class AndOperator < Treetop::Runtime::SyntaxNode
+ def match( cond1, cond2 )
+ cond1 and cond2
+ end
+ end
+
+ # we don't support fuzzy string matching
+ class FuzzyOp < Treetop::Runtime::SyntaxNode
+ end
+
+ class BoostOp < Treetop::Runtime::SyntaxNode
+ end
+
+ class FuzzyParam < Treetop::Runtime::SyntaxNode
+ end
+
+ class UnaryOp < Treetop::Runtime::SyntaxNode
+ def match( item )
+ self.elements[0].match(
+ self.elements[1].match(item)
+ )
+ end
+ end
+
+ class NotOperator < Treetop::Runtime::SyntaxNode
+ def match( cond )
+ not cond
+ end
+ end
+
+ class RequiredOperator < Treetop::Runtime::SyntaxNode
+ end
+
+ class ProhibitedOperator < Treetop::Runtime::SyntaxNode
+ end
+
+ class Phrase < Treetop::Runtime::SyntaxNode
+ # a quoted ::Term
+ def match( value )
+ self.elements[0].match(value)
+ end
+ end
+end
+
+class Query
+ # initialize the parser by using the grammar shipped with chef
+ @@grammar = File.join(Chef::SolrQuery::QueryTransform.base_path, "lucene.treetop")
+ Treetop.load(@@grammar)
+ @@parser = LuceneParser.new
+
+ def self.parse(data)
+ # parse the query into a query tree
+ if data.nil?
+ data = "*:*"
+ end
+ tree = @@parser.parse(data)
+ if tree.nil?
+ msg = "Parse error at offset: #{@@parser.index}\n"
+ msg += "Reason: #{@@parser.failure_reason}"
+ raise "Query #{data} is not supported: #{msg}"
+ end
+ self.clean_tree(tree)
+ tree
+ end
+
+ private
+
+ def self.clean_tree(root_node)
+ # remove all SyntaxNode elements from the tree, we don't need them as
+ # the related ruby class already knowns what to do.
+ return if root_node.elements.nil?
+ root_node.elements.delete_if do |node|
+ node.class.name == "Treetop::Runtime::SyntaxNode"
+ end
+ root_node.elements.each { |node| self.clean_tree(node) }
+ end
+end
+
View
159 littlechef/search.rb
@@ -34,163 +34,8 @@ def require_relative(relative_feature)
end
require_relative 'data_bags.rb'
+ require_relative 'parser.rb'
- # Checks if a given `value` is equal to `match`
- # If value is an Array, then `match` is checked against each of the value's
- # members, and true is returned if any of the members matches.
- # The comparison is string based, means: if value is not an Array then value
- # gets converted into a string (using .to_s) and then checked.
- def match_value(value, match)
- if value.is_a?(Array)
- return value.any?{ |x| match_value(x, match) }
- else
- return value.to_s == match
- end
- end
-
- # Factory function to parse the query string into a `Query` object
- # Returns `nil` if the query is not supported.
- def make_query(query)
- if query.nil? or query === "*:*"
- return NilQuery.new(query)
- end
-
- query.gsub!("[* TO *]", "*")
- if query.count("()") == 2 and query.start_with?("(") and query.end_with?(")")
- query.tr!("()", "")
- end
-
- if query.include?(" AND ")
- return AndQuery.new(query.split(" AND ").collect{ |x| make_query(x) })
- elsif query.include?(" OR ")
- return OrQuery.new(query.split(" OR ").collect{ |x| make_query(x) })
- elsif query.include?(" NOT ")
- return NotQuery.new(query.split(" NOT ").collect{ |x| make_query(x) })
- end
-
- if query.start_with?("NOT")
- negation = true
- query = query[3..-1] # strip leading NOT
- else
- negation = false
- end
-
- if query.split(":", 2).length == 2
- field, query_string = query.split(":", 2)
- if query_string.end_with?("*")
- return WildCardFieldQuery.new(query, negation)
- else
- return FieldQuery.new(query, negation)
- end
- else
- return nil
- end
- end
-
- # BaseClass for all queries
- class Query
- def initialize( query )
- @query = query
- end
-
- def match( item )
- return false
- end
- end
-
- # BaseClass for all queries with AND, OR or NOT conditions
- class NestedQuery < Query
- def initialize( conditions )
- @conditions = conditions
- end
- end
-
- # AndQuery matches if all sub-queries match
- class AndQuery < NestedQuery
- def match( item )
- return @conditions.all?{ |x| x.match(item) }
- end
- end
-
- # NotQuery matches if the *leftmost* condition matches, but the others don't
- class NotQuery < NestedQuery
- def match( item )
- base_condition = @conditions[0]
- last_conditions = @conditions[1..-1] || []
- return base_condition.match(item) & last_conditions.all?{ |x| !x.match(item) }
- end
- end
-
- # OrQuery matches if any of the sub-queries match
- class OrQuery < NestedQuery
- def match( item )
- return @conditions.any?{ |x| x.match(item) }
- end
- end
-
- # NilQuery always matches
- class NilQuery < Query
- def match( item )
- return true
- end
- end
-
- # FieldQuery looks for a certain attribute in the item to match and checks
- # the value of this attribute for equality.
- # If `negation` is true the oposite result will be returned.
- class FieldQuery < Query
- def initialize( query, negation=false )
- @field, @query = query.split(":", 2)
- @negation = negation
- @field.strip!
- end
-
- def match( item )
- value = item[@field]
- if value.nil?
- result = false
- end
- result = match_value(value, @query)
- if @negation
- return !result
- else
- return result
- end
- end
- end
-
- # WildCardFieldQuery is exactly like FieldQuery, but allows trailing stars.
- # Instead of checking for exact matches it just checks if the value begins
- # with a certain string, or (in case of an Array) any of its items value
- # begins with a string.
- class WildCardFieldQuery < FieldQuery
-
- def initialize( query, negation=false )
- super
- @query = @query.chop
- end
-
- def match( item )
- value = item[@field]
- if value.nil?
- result = false
- elsif value.is_a?(String)
- if value.strip().empty?
- result = true
- else
- result = value.start_with?(@query)
- end
- else
- result = value.any?{ |x| x.start_with?(@query) }
- end
- if @negation
- return !result
- else
- return result
- end
- end
- end
-
class Chef
class Recipe
@@ -205,7 +50,7 @@ def search(bag_name, query=nil, sort=nil, start=0, rows=1000, &block)
if !sort.nil?
raise "Sorting search results is not supported"
end
- @_query = make_query(query)
+ @_query = Query.parse(query)
if @_query.nil?
raise "Query #{query} is not supported"
end
View
4 setup.py
@@ -21,7 +21,9 @@
keywords=["chef", "devops"],
install_requires=['fabric>=1.0.2', 'simplejson'],
packages=['littlechef'],
- package_data={'littlechef': ['data_bags.rb', 'search.rb', 'solo.rb']},
+ package_data={
+ 'littlechef': ['data_bags.rb', 'search.rb', 'solo.rb', 'parser.rb']
+ },
scripts=['cook'],
test_suite='nose.collector',
classifiers=[
Please sign in to comment.
Something went wrong with that request. Please try again.