mongo-parser-rb 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongo-parser-rb.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ben McRedmond
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # MongoParserRB
2
+
3
+ Parse and evaluate MongoDB queries in Ruby. MongoParserRB is useful for checking if already loaded documents match a query, without the overhead of making requests to the database.
4
+
5
+ Parse a query:
6
+
7
+ ```ruby
8
+ q = MongoParserRB::Query.parse({:comment_count => 5})
9
+ ```
10
+
11
+ Once a query has been parsed you can check if individual documents match a query:
12
+
13
+ ```ruby
14
+ q.matches_document?({:comment_count => 4}) => false
15
+ q.matches_document?({:comment_count => 5}) => true
16
+ ```
17
+
18
+ You can use MongoDB's conditional operators, specify them as symbols:
19
+
20
+ ```ruby
21
+ q = MongoParserRB::Query.parse({:comment_count => {:$gt => 5, :$lt => 10}})
22
+ q.matches_document?({:comment_count => 11}) => false
23
+ q.matches_document?({:comment_count => 6}) => true
24
+ ```
25
+
26
+ The following operators are currently supported: `$and`, `$or`, `$in`, `$nin`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`.
27
+
28
+ Regexps are also supported:
29
+
30
+ ```ruby
31
+ q = MongoParserRB::Query.parse({:"email" => /gmail/})
32
+ q.matches_document?({:email => "ben@intercom.io"}) => false
33
+ q.matches_document?({:email => "ciaran@gmail.com"}) => true
34
+ ```
35
+
36
+ MongoDB's dot field syntax can be used:
37
+
38
+ ```ruby
39
+ q = MongoParserRB::Query.parse({:"author.name" => "Ben"})
40
+ q.matches_document?({:author => {:name => "Ciaran"}}) => false
41
+ q.matches_document?({:author => {:name => "Ben"}}) => true
42
+ ```
43
+
44
+ ## Installation
45
+
46
+ Add this line to your application's Gemfile:
47
+
48
+ gem 'mongo-parser-rb'
49
+
50
+ And then execute:
51
+
52
+ $ bundle
53
+
54
+ Or install it yourself as:
55
+
56
+ $ gem install mongo-parser-rb
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new("test") do |test|
5
+ test.libs << 'lib'
6
+ test.libs << 'test'
7
+
8
+ test.test_files = FileList['test/**/*_test.rb']
9
+ test.warning = false
10
+ test.verbose = true
11
+ end
@@ -0,0 +1,5 @@
1
+ module MongoParserRB
2
+
3
+ class NotParsedError < StandardError; end
4
+
5
+ end
@@ -0,0 +1,39 @@
1
+ module MongoParserRB
2
+ class Field
3
+
4
+ def initialize(field)
5
+ @field = field
6
+ @field_parts = field.to_s.split('.')
7
+ end
8
+
9
+ def value_in_document(document)
10
+ document = stringify_keys(document)
11
+ @field_parts.reduce(document) do |value, field|
12
+ value[field]
13
+ end
14
+ rescue NoMethodError
15
+ nil
16
+ end
17
+
18
+ def in_document?(document)
19
+ document = stringify_keys(document)
20
+
21
+ @field_parts.reduce(document) do |value, field|
22
+ return false unless value.has_key?(field)
23
+ value[field]
24
+ end
25
+
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def stringify_keys(document)
32
+ document.reduce({}) do |new_document, (k,v)|
33
+ new_document[k.to_s] = v.kind_of?(Hash) ? stringify_keys(v) : v
34
+ new_document
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,126 @@
1
+ module MongoParserRB
2
+ class Query
3
+ class Expression
4
+
5
+ class << self
6
+
7
+ def conjunction_operators
8
+ @conjunction_operators ||= [
9
+ :$and,
10
+ :$or
11
+ ]
12
+ end
13
+
14
+ def conjunction_operator?(operator)
15
+ conjunction_operators.include?(operator)
16
+ end
17
+
18
+ def negative_equality_operators
19
+ @negative_equality_operators ||= [
20
+ :$nin,
21
+ :$ne
22
+ ]
23
+ end
24
+
25
+ def equality_operators
26
+ @equality_operators ||= [
27
+ :$eq,
28
+ :$gt,
29
+ :$lt,
30
+ :$gte,
31
+ :$lte,
32
+ :$in
33
+ ] | negative_equality_operators
34
+ end
35
+
36
+ def equality_operator?(operator)
37
+ equality_operators.include?(operator)
38
+ end
39
+
40
+ def operator?(operator)
41
+ equality_operator?(operator) || conjunction_operator?(operator)
42
+ end
43
+
44
+ end
45
+
46
+ def initialize(operator, *args)
47
+ @operator = operator
48
+
49
+ if Expression.conjunction_operator? @operator
50
+ @arguments = args[0]
51
+ else
52
+ @field = Field.new(args[0])
53
+ @arguments = args[1]
54
+ end
55
+ end
56
+
57
+ def evaluate(document)
58
+ case @operator
59
+ when *Expression.conjunction_operators
60
+ evaluate_conjunction(document)
61
+ when *Expression.negative_equality_operators
62
+ evaluate_negative_equality(document)
63
+ when *Expression.equality_operators
64
+ evaluate_equality(document)
65
+ end
66
+ rescue NoMethodError
67
+ false
68
+ end
69
+
70
+ private
71
+
72
+ def evaluate_conjunction(document)
73
+ case @operator
74
+ when :$and
75
+ @arguments.all? do |arg|
76
+ arg.evaluate(document)
77
+ end
78
+ when :$or
79
+ @arguments.any? do |arg|
80
+ arg.evaluate(document)
81
+ end
82
+ end
83
+ end
84
+
85
+ def evaluate_negative_equality(document)
86
+ value_for_field = @field.value_in_document(document)
87
+
88
+ # Mongo negative equality operators return true when
89
+ # the specified field does not exist on a document.
90
+ return true if !value_for_field && !@field.in_document?(document)
91
+
92
+ case @operator
93
+ when :$ne
94
+ value_for_field != @arguments
95
+ when :$nin
96
+ (value_for_field & @arguments).length.zero?
97
+ end
98
+ end
99
+
100
+ def evaluate_equality(document)
101
+ value_for_field = @field.value_in_document(document)
102
+ return false if !value_for_field && !@field.in_document?(document)
103
+
104
+ case @operator
105
+ when :$eq
106
+ if @arguments.kind_of?(Regexp)
107
+ !!(value_for_field =~ @arguments)
108
+ else
109
+ value_for_field == @arguments
110
+ end
111
+ when :$gt
112
+ value_for_field > @arguments
113
+ when :$gte
114
+ value_for_field >= @arguments
115
+ when :$lt
116
+ value_for_field < @arguments
117
+ when :$lte
118
+ value_for_field <= @arguments
119
+ when :$in
120
+ (value_for_field & @arguments).length > 0
121
+ end
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,46 @@
1
+ module MongoParserRB
2
+ class Query
3
+
4
+ def self.parse(raw_query)
5
+ new(raw_query).parse!
6
+ end
7
+
8
+ def initialize(raw_query)
9
+ @raw_query = raw_query
10
+ end
11
+
12
+ def parse!
13
+ @expression_tree = parse_root_expression(@raw_query)
14
+ self
15
+ end
16
+
17
+ def matches_document?(document)
18
+ raise NotParsedError, "Query not parsed (run parse!)" if @expression_tree.nil?
19
+ @expression_tree.evaluate(document)
20
+ end
21
+
22
+ private
23
+
24
+ def parse_root_expression(query, field = nil)
25
+ Expression.new(:$and, query.to_a.map do |(key, value)|
26
+ parse_sub_expression(key, value, field)
27
+ end)
28
+ end
29
+
30
+ def parse_sub_expression(key, value, field = nil)
31
+ if Expression.operator?(key)
32
+ case key
33
+ when *Expression.conjunction_operators
34
+ Expression.new(key, value.map { |v| parse_root_expression(v) })
35
+ else
36
+ Expression.new(key, field, value)
37
+ end
38
+ elsif value.kind_of?(Hash)
39
+ parse_root_expression(value, key)
40
+ else
41
+ Expression.new(:$eq, key, value)
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module MongoParserRB
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "mongo-parser-rb/version"
2
+ require "mongo-parser-rb/exceptions"
3
+ require "mongo-parser-rb/field"
4
+ require "mongo-parser-rb/query/expression"
5
+ require "mongo-parser-rb/query"
6
+
7
+ module MongoParserRB
8
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongo-parser-rb/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "mongo-parser-rb"
8
+ gem.version = MongoParserRB::VERSION
9
+ gem.authors = ["Ben McRedmond"]
10
+ gem.email = ["ben@intercom.io"]
11
+ gem.description = %q{Parse and evaluate mongo queries in Ruby}
12
+ gem.summary = gem.description
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class FieldTest < MiniTest::Unit::TestCase
4
+
5
+ def test_returns_value_when_key_present
6
+ field = MongoParserRB::Field.new(:"custom_data.tracked_users")
7
+ assert_equal 10, field.value_in_document(:custom_data => {:tracked_users => 10})
8
+ end
9
+
10
+ def test_returns_nil_when_key_not_present
11
+ field = MongoParserRB::Field.new(:"custom_data.tracked_users")
12
+ assert_nil field.value_in_document(:custom_data => {:something_else => 10})
13
+ assert_nil field.value_in_document(:name => "Ben")
14
+ end
15
+
16
+ def test_document_has_a_field
17
+ field = MongoParserRB::Field.new(:"custom_data.tracked_users")
18
+ assert field.in_document?(:custom_data => {:tracked_users => 10})
19
+ refute field.in_document?(:not_custom_data => {:tracked_users => 10})
20
+
21
+ field = MongoParserRB::Field.new(:"session_count")
22
+ assert field.in_document?(:custom_data => {:tracked_users => 10}, :session_count => 5)
23
+ refute field.in_document?(:custom_data => {:tracked_users => 10})
24
+ end
25
+
26
+ end
@@ -0,0 +1,152 @@
1
+ require 'test_helper'
2
+
3
+ class QueryTest < MiniTest::Unit::TestCase
4
+
5
+ def test_raises_if_not_parsed
6
+ assert_raises(MongoParserRB::NotParsedError) do
7
+ query = MongoParserRB::Query.new(:integer_key => 10)
8
+ query.matches_document?({})
9
+ end
10
+ end
11
+
12
+ def test_integer_eq
13
+ query = MongoParserRB::Query.parse(:integer_key => 10)
14
+ assert query.matches_document?(:integer_key => 10)
15
+ refute query.matches_document?(:integer_key => 9)
16
+ end
17
+
18
+ def test_integer_ne
19
+ query = MongoParserRB::Query.parse(:integer_key => {:$ne => 10})
20
+ assert query.matches_document?(:integer_key => 9)
21
+ refute query.matches_document?(:integer_key => 10)
22
+ end
23
+
24
+ def test_integer_gt
25
+ query = MongoParserRB::Query.parse(:integer_key => {:$gt => 10})
26
+ assert query.matches_document?(:integer_key => 11)
27
+ refute query.matches_document?(:integer_key => 10)
28
+ refute query.matches_document?(:integer_key => 9)
29
+ end
30
+
31
+ def test_integer_lt
32
+ query = MongoParserRB::Query.parse(:integer_key => {:$lt => 10})
33
+ query.matches_document?(:integer_key => 9)
34
+ query.matches_document?(:integer_key => 10)
35
+ query.matches_document?(:integer_key => 11)
36
+ end
37
+
38
+ def test_integer_lte
39
+ query = MongoParserRB::Query.parse(:integer_key => {:$lte => 10})
40
+ assert query.matches_document?(:integer_key => 9)
41
+ assert query.matches_document?(:integer_key => 10)
42
+ refute query.matches_document?(:integer_key => 11)
43
+ end
44
+
45
+ def test_integer_gte
46
+ query = MongoParserRB::Query.parse(:integer_key => {:$lte => 10})
47
+ assert query.matches_document?(:integer_key => 9)
48
+ assert query.matches_document?(:integer_key => 10)
49
+ refute query.matches_document?(:integer_key => 11)
50
+ end
51
+
52
+ def test_integer_lte_and_integer_gt
53
+ query = MongoParserRB::Query.parse(:integer_key => {:$lte => 10}, :integer_key_2 => {:$gt => 5})
54
+ assert query.matches_document?(:integer_key => 9, :integer_key_2 => 6)
55
+ refute query.matches_document?(:integer_key => 9, :integer_key_2 => 4)
56
+ refute query.matches_document?(:integer_key => 11, :integer_key_2 => 4)
57
+ end
58
+
59
+ def test_string_eq
60
+ query = MongoParserRB::Query.parse(:string_key => "hello world")
61
+ assert query.matches_document?(:string_key => "hello world")
62
+ refute query.matches_document?(:string_key => 1)
63
+ refute query.matches_document?(:string_key => "bye world")
64
+ end
65
+
66
+ def test_string_gt
67
+ query = MongoParserRB::Query.parse(:string_key => {:$gt => 'abc'})
68
+ assert query.matches_document?(:string_key => "abcd")
69
+ assert query.matches_document?(:string_key => "e")
70
+ refute query.matches_document?(:string_key => "abc")
71
+ end
72
+
73
+ def test_string_lt
74
+ query = MongoParserRB::Query.parse(:string_key => {:$lt => 'm'})
75
+ assert query.matches_document?(:string_key => "abc")
76
+ refute query.matches_document?(:string_key => "xyz")
77
+ end
78
+
79
+ def test_string_and_integer_equality
80
+ query = MongoParserRB::Query.parse(:string_key => {:$lt => 'm'}, :integer_key => {:$gt => 4})
81
+ assert query.matches_document?(:string_key => "abc", :integer_key => 5)
82
+ refute query.matches_document?(:string_key => "xyz", :integer_key => 5)
83
+ refute query.matches_document?(:string_key => "abc", :integer_key => 4)
84
+ end
85
+
86
+
87
+ def test_or
88
+ query = MongoParserRB::Query.parse(:$or => [
89
+ {:string_key => "abc"},
90
+ {:integer_key => {:$gt => 5}}
91
+ ])
92
+
93
+ assert query.matches_document?(:string_key => "abc")
94
+ refute query.matches_document?(:string_key => "cde")
95
+ assert query.matches_document?(:string_key => "cde", :integer_key => 6)
96
+ assert query.matches_document?(:string_key => "abc", :integer_key => 6)
97
+ assert query.matches_document?(:integer_key => 6)
98
+ refute query.matches_document?(:integer_key => 5)
99
+ end
100
+
101
+ def test_boolean_eq
102
+ query = MongoParserRB::Query.parse(:boolean_key => false)
103
+ assert query.matches_document?(:boolean_key => false)
104
+ refute query.matches_document?(:boolean_key => true)
105
+ end
106
+
107
+ def test_query_field_integration
108
+ query = MongoParserRB::Query.parse(:"custom_data.tracked_users" => {:$gt => 3})
109
+ assert query.matches_document?(:custom_data => {:tracked_users => 10})
110
+ refute query.matches_document?(:custom_data => {:tracked_users => 1})
111
+ end
112
+
113
+ def test_array_in
114
+ query = MongoParserRB::Query.parse(:array_key => {:$in => [1]})
115
+ assert query.matches_document?(:array_key => [1,2])
116
+ refute query.matches_document?(:array_key => [2,3])
117
+ end
118
+
119
+ def test_array_nin
120
+ query = MongoParserRB::Query.parse(:array_key => {:$nin => [1,2]})
121
+ assert query.matches_document?(:array_key => [3,4,5])
122
+ refute query.matches_document?(:array_key => [1,4,5])
123
+ end
124
+
125
+ def test_eq_nil
126
+ query = MongoParserRB::Query.parse(:string_key => nil)
127
+ assert query.matches_document?(:string_key => nil)
128
+ refute query.matches_document?(:string_key => 'hey')
129
+ end
130
+
131
+ def test_regex_eq
132
+ query = MongoParserRB::Query.parse(:string_key => /hello/)
133
+ assert query.matches_document?(:string_key => 'hello world')
134
+ refute query.matches_document?(:string_key => 'world')
135
+ end
136
+
137
+ def test_operator_data_type_mismatch
138
+ query = MongoParserRB::Query.parse(:array_key => {:$in => [1]})
139
+ refute query.matches_document?(:array_key => "hey")
140
+ end
141
+
142
+ def test_date_range
143
+ query = MongoParserRB::Query.parse(:date_key => {:$gt => Time.new(1993,2,13,0,0,0)})
144
+ assert query.matches_document?(:date_key => Time.new(1994,2,13,0,0,0))
145
+ refute query.matches_document?(:date_key => Time.new(1992,2,13,0,0,0))
146
+
147
+ query = MongoParserRB::Query.parse(:date_key => {:$gt => Time.new(1993,2,13,0,0,0), :$lt => Time.new(1995,2,13,0,0,0)})
148
+ assert query.matches_document?(:date_key => Time.new(1994,2,13,0,0,0))
149
+ refute query.matches_document?(:date_key => Time.new(1996,2,13,0,0,0))
150
+ end
151
+
152
+ end
@@ -0,0 +1,2 @@
1
+ require 'mongo-parser-rb'
2
+ require 'minitest/autorun'
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongo-parser-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben McRedmond
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-27 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Parse and evaluate mongo queries in Ruby
15
+ email:
16
+ - ben@intercom.io
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/mongo-parser-rb.rb
27
+ - lib/mongo-parser-rb/exceptions.rb
28
+ - lib/mongo-parser-rb/field.rb
29
+ - lib/mongo-parser-rb/query.rb
30
+ - lib/mongo-parser-rb/query/expression.rb
31
+ - lib/mongo-parser-rb/version.rb
32
+ - mongo-parser-rb.gemspec
33
+ - test/mongo-parser-rb/field_test.rb
34
+ - test/mongo-parser-rb/query_test.rb
35
+ - test/test_helper.rb
36
+ homepage: ''
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.25
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Parse and evaluate mongo queries in Ruby
60
+ test_files:
61
+ - test/mongo-parser-rb/field_test.rb
62
+ - test/mongo-parser-rb/query_test.rb
63
+ - test/test_helper.rb
64
+ has_rdoc: