mongomodel 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.
Files changed (108) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +34 -0
  3. data/Rakefile +47 -0
  4. data/bin/console +45 -0
  5. data/lib/mongomodel.rb +92 -0
  6. data/lib/mongomodel/attributes/mongo.rb +40 -0
  7. data/lib/mongomodel/attributes/store.rb +30 -0
  8. data/lib/mongomodel/attributes/typecasting.rb +51 -0
  9. data/lib/mongomodel/concerns/abstract_class.rb +17 -0
  10. data/lib/mongomodel/concerns/activemodel.rb +11 -0
  11. data/lib/mongomodel/concerns/associations.rb +103 -0
  12. data/lib/mongomodel/concerns/associations/base/association.rb +33 -0
  13. data/lib/mongomodel/concerns/associations/base/definition.rb +56 -0
  14. data/lib/mongomodel/concerns/associations/base/proxy.rb +58 -0
  15. data/lib/mongomodel/concerns/associations/belongs_to.rb +68 -0
  16. data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +159 -0
  17. data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +175 -0
  18. data/lib/mongomodel/concerns/attribute_methods.rb +55 -0
  19. data/lib/mongomodel/concerns/attribute_methods/before_type_cast.rb +29 -0
  20. data/lib/mongomodel/concerns/attribute_methods/dirty.rb +35 -0
  21. data/lib/mongomodel/concerns/attribute_methods/protected.rb +127 -0
  22. data/lib/mongomodel/concerns/attribute_methods/query.rb +22 -0
  23. data/lib/mongomodel/concerns/attribute_methods/read.rb +29 -0
  24. data/lib/mongomodel/concerns/attribute_methods/write.rb +29 -0
  25. data/lib/mongomodel/concerns/attributes.rb +85 -0
  26. data/lib/mongomodel/concerns/callbacks.rb +294 -0
  27. data/lib/mongomodel/concerns/logging.rb +15 -0
  28. data/lib/mongomodel/concerns/pretty_inspect.rb +29 -0
  29. data/lib/mongomodel/concerns/properties.rb +69 -0
  30. data/lib/mongomodel/concerns/record_status.rb +42 -0
  31. data/lib/mongomodel/concerns/timestamps.rb +32 -0
  32. data/lib/mongomodel/concerns/validations.rb +38 -0
  33. data/lib/mongomodel/concerns/validations/associated.rb +46 -0
  34. data/lib/mongomodel/document.rb +20 -0
  35. data/lib/mongomodel/document/callbacks.rb +46 -0
  36. data/lib/mongomodel/document/dynamic_finders.rb +88 -0
  37. data/lib/mongomodel/document/finders.rb +82 -0
  38. data/lib/mongomodel/document/indexes.rb +91 -0
  39. data/lib/mongomodel/document/optimistic_locking.rb +48 -0
  40. data/lib/mongomodel/document/persistence.rb +143 -0
  41. data/lib/mongomodel/document/scopes.rb +161 -0
  42. data/lib/mongomodel/document/validations.rb +68 -0
  43. data/lib/mongomodel/document/validations/uniqueness.rb +78 -0
  44. data/lib/mongomodel/embedded_document.rb +42 -0
  45. data/lib/mongomodel/locale/en.yml +55 -0
  46. data/lib/mongomodel/support/collection.rb +109 -0
  47. data/lib/mongomodel/support/configuration.rb +35 -0
  48. data/lib/mongomodel/support/core_extensions.rb +10 -0
  49. data/lib/mongomodel/support/exceptions.rb +25 -0
  50. data/lib/mongomodel/support/mongo_options.rb +177 -0
  51. data/lib/mongomodel/support/types.rb +35 -0
  52. data/lib/mongomodel/support/types/array.rb +11 -0
  53. data/lib/mongomodel/support/types/boolean.rb +25 -0
  54. data/lib/mongomodel/support/types/custom.rb +38 -0
  55. data/lib/mongomodel/support/types/date.rb +20 -0
  56. data/lib/mongomodel/support/types/float.rb +13 -0
  57. data/lib/mongomodel/support/types/hash.rb +18 -0
  58. data/lib/mongomodel/support/types/integer.rb +13 -0
  59. data/lib/mongomodel/support/types/object.rb +21 -0
  60. data/lib/mongomodel/support/types/string.rb +9 -0
  61. data/lib/mongomodel/support/types/symbol.rb +9 -0
  62. data/lib/mongomodel/support/types/time.rb +12 -0
  63. data/lib/mongomodel/version.rb +3 -0
  64. data/spec/mongomodel/attributes/store_spec.rb +273 -0
  65. data/spec/mongomodel/concerns/activemodel_spec.rb +61 -0
  66. data/spec/mongomodel/concerns/associations/belongs_to_spec.rb +153 -0
  67. data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +165 -0
  68. data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +192 -0
  69. data/spec/mongomodel/concerns/attribute_methods/before_type_cast_spec.rb +46 -0
  70. data/spec/mongomodel/concerns/attribute_methods/dirty_spec.rb +131 -0
  71. data/spec/mongomodel/concerns/attribute_methods/protected_spec.rb +86 -0
  72. data/spec/mongomodel/concerns/attribute_methods/query_spec.rb +27 -0
  73. data/spec/mongomodel/concerns/attribute_methods/read_spec.rb +52 -0
  74. data/spec/mongomodel/concerns/attribute_methods/write_spec.rb +43 -0
  75. data/spec/mongomodel/concerns/attributes_spec.rb +152 -0
  76. data/spec/mongomodel/concerns/callbacks_spec.rb +90 -0
  77. data/spec/mongomodel/concerns/logging_spec.rb +20 -0
  78. data/spec/mongomodel/concerns/pretty_inspect_spec.rb +68 -0
  79. data/spec/mongomodel/concerns/properties_spec.rb +29 -0
  80. data/spec/mongomodel/concerns/timestamps_spec.rb +170 -0
  81. data/spec/mongomodel/concerns/validations_spec.rb +159 -0
  82. data/spec/mongomodel/document/callbacks_spec.rb +80 -0
  83. data/spec/mongomodel/document/dynamic_finders_spec.rb +183 -0
  84. data/spec/mongomodel/document/finders_spec.rb +231 -0
  85. data/spec/mongomodel/document/indexes_spec.rb +121 -0
  86. data/spec/mongomodel/document/optimistic_locking_spec.rb +57 -0
  87. data/spec/mongomodel/document/persistence_spec.rb +319 -0
  88. data/spec/mongomodel/document/scopes_spec.rb +204 -0
  89. data/spec/mongomodel/document/validations/uniqueness_spec.rb +217 -0
  90. data/spec/mongomodel/document/validations_spec.rb +132 -0
  91. data/spec/mongomodel/document_spec.rb +74 -0
  92. data/spec/mongomodel/embedded_document_spec.rb +66 -0
  93. data/spec/mongomodel/mongomodel_spec.rb +33 -0
  94. data/spec/mongomodel/support/collection_spec.rb +248 -0
  95. data/spec/mongomodel/support/mongo_options_spec.rb +295 -0
  96. data/spec/mongomodel/support/property_spec.rb +83 -0
  97. data/spec/spec.opts +6 -0
  98. data/spec/spec_helper.rb +21 -0
  99. data/spec/specdoc.opts +6 -0
  100. data/spec/support/callbacks.rb +44 -0
  101. data/spec/support/helpers/define_class.rb +24 -0
  102. data/spec/support/helpers/specs_for.rb +11 -0
  103. data/spec/support/matchers/be_a_subclass_of.rb +5 -0
  104. data/spec/support/matchers/respond_to_boolean.rb +17 -0
  105. data/spec/support/matchers/run_callbacks.rb +20 -0
  106. data/spec/support/models.rb +23 -0
  107. data/spec/support/time.rb +6 -0
  108. metadata +232 -0
@@ -0,0 +1,78 @@
1
+ module MongoModel
2
+ module DocumentExtensions
3
+ module Validations
4
+ module ClassMethods
5
+ # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
6
+ # can be named "davidhh".
7
+ #
8
+ # class Person < MongoModel::Document
9
+ # validates_uniqueness_of :user_name, :scope => :account_id
10
+ # end
11
+ #
12
+ # It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
13
+ # making sure that a teacher can only be on the schedule once per semester for a particular class.
14
+ #
15
+ # class TeacherSchedule < MongoModel::Document
16
+ # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
17
+ # end
18
+ #
19
+ # When the document is created, a check is performed to make sure that no document exists in the database with the given value for the specified
20
+ # attribute (that maps to a property). When the document is updated, the same check is made but disregarding the document itself.
21
+ #
22
+ # Configuration options:
23
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
24
+ # * <tt>:scope</tt> - One or more properties by which to limit the scope of the uniqueness constraint.
25
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
26
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
27
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
28
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
29
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
30
+ # method, proc or string should return or evaluate to a true or false value.
31
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
32
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
33
+ # method, proc or string should return or evaluate to a true or false value.
34
+ #
35
+ # === Concurrency and integrity
36
+ #
37
+ # Note that this validation method does not have the same race condition suffered by ActiveRecord and other ORMs.
38
+ # A unique index is added to the collection to ensure that the collection never ends up in an invalid state.
39
+ def validates_uniqueness_of(*attr_names)
40
+ configuration = { :case_sensitive => true }
41
+ configuration.update(attr_names.extract_options!)
42
+
43
+ # Enable safety checks on save
44
+ self.save_safely = true
45
+
46
+ # Create unique indexes to deal with race condition
47
+ attr_names.each do |attr_name|
48
+ if configuration[:case_sensitive]
49
+ index *[attr_name] + Array(configuration[:scope]) << { :unique => true }
50
+ else
51
+ lowercase_key = "_lowercase_#{attr_name}"
52
+ before_save { attributes[lowercase_key] = send(attr_name).downcase }
53
+ index *[lowercase_key] + Array(configuration[:scope]) << { :unique => true }
54
+ end
55
+ end
56
+
57
+ validates_each(attr_names, configuration) do |record, attr_name, value|
58
+ if configuration[:case_sensitive] || !value.is_a?(String)
59
+ conditions = { attr_name => value }
60
+ else
61
+ conditions = { "_lowercase_#{attr_name}" => value.downcase }
62
+ end
63
+
64
+ Array(configuration[:scope]).each do |scope|
65
+ conditions[scope] = record.send(scope)
66
+ end
67
+
68
+ conditions.merge!(:id.ne => record.id) unless record.new_record?
69
+
70
+ if exists?(conditions)
71
+ record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,42 @@
1
+ module MongoModel
2
+ class EmbeddedDocument
3
+ def to_param
4
+ id
5
+ end
6
+
7
+ def ==(other)
8
+ other.is_a?(self.class) && other.attributes == attributes
9
+ end
10
+
11
+ include Attributes
12
+ include Properties
13
+
14
+ include Validations
15
+ include Callbacks
16
+
17
+ include Associations
18
+
19
+ include AttributeMethods
20
+ include AttributeMethods::Read
21
+ include AttributeMethods::Write
22
+ include AttributeMethods::Query
23
+ include AttributeMethods::BeforeTypeCast
24
+ include AttributeMethods::Protected
25
+ include AttributeMethods::Dirty
26
+
27
+ include Logging
28
+ include RecordStatus
29
+ include ActiveModelCompatibility
30
+ include Timestamps
31
+ include PrettyInspect
32
+ include AbstractClass
33
+
34
+ # Allow Collection class to be used in property definitions
35
+ Collection = MongoModel::Collection
36
+
37
+ undef_method :type if method_defined?(:type)
38
+ property :type, String, :as => '_type', :default => lambda { |doc| doc.class.name }, :protected => true
39
+
40
+ self.abstract_class = true
41
+ end
42
+ end
@@ -0,0 +1,55 @@
1
+ en:
2
+ mongomodel:
3
+ errors:
4
+ # The values :model, :attribute and :value are always available for interpolation
5
+ # The value :count is available when applicable. Can be used for pluralization.
6
+ messages:
7
+ inclusion: "is not included in the list"
8
+ exclusion: "is reserved"
9
+ invalid: "is invalid"
10
+ confirmation: "doesn't match confirmation"
11
+ accepted: "must be accepted"
12
+ empty: "can't be empty"
13
+ blank: "can't be blank"
14
+ too_long: "is too long (maximum is {{count}} characters)"
15
+ too_short: "is too short (minimum is {{count}} characters)"
16
+ wrong_length: "is the wrong length (should be {{count}} characters)"
17
+ taken: "has already been taken"
18
+ not_a_number: "is not a number"
19
+ greater_than: "must be greater than {{count}}"
20
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
21
+ equal_to: "must be equal to {{count}}"
22
+ less_than: "must be less than {{count}}"
23
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
24
+ odd: "must be odd"
25
+ even: "must be even"
26
+ document_invalid: "Validation failed: {{errors}}"
27
+ # Append your own errors here or at the model/attributes scope.
28
+
29
+ # You can define own errors for models or model attributes.
30
+ # The values :model, :attribute and :value are always available for interpolation.
31
+ #
32
+ # For example,
33
+ # models:
34
+ # user:
35
+ # blank: "This is a custom blank message for {{model}}: {{attribute}}"
36
+ # attributes:
37
+ # login:
38
+ # blank: "This is a custom blank message for User login"
39
+ # Will define custom blank validation message for User model and
40
+ # custom blank validation message for login attribute of User model.
41
+ #models:
42
+
43
+ # Translate model names. Used in Model.human_name().
44
+ #models:
45
+ # For example,
46
+ # user: "Dude"
47
+ # will translate User model name to "Dude"
48
+
49
+ # Translate model attribute names. Used in Model.human_attribute_name(attribute).
50
+ #attributes:
51
+ # For example,
52
+ # user:
53
+ # login: "Handle"
54
+ # will translate User attribute "login" as "Handle"
55
+
@@ -0,0 +1,109 @@
1
+ module MongoModel
2
+ class Collection < Array
3
+ ARRAY_CONVERTER = Types.converter_for(Array)
4
+
5
+ class_inheritable_accessor :type
6
+ self.type = Object
7
+
8
+ def initialize(array=[])
9
+ super(array.map { |i| convert(i) })
10
+ end
11
+
12
+ def []=(index, value)
13
+ super(index, convert(value))
14
+ end
15
+
16
+ def <<(value)
17
+ super(convert(value))
18
+ end
19
+
20
+ def +(other)
21
+ self.class.new(super(other))
22
+ end
23
+
24
+ def concat(values)
25
+ super(values.map { |v| convert(v) })
26
+ end
27
+
28
+ def delete(value)
29
+ super(convert(value))
30
+ end
31
+
32
+ def include?(value)
33
+ super(convert(value))
34
+ end
35
+
36
+ def index(value)
37
+ super(convert(value))
38
+ end
39
+
40
+ def insert(index, value)
41
+ super(index, convert(value))
42
+ end
43
+
44
+ def push(*values)
45
+ super(*values.map { |v| convert(v) })
46
+ end
47
+
48
+ def rindex(value)
49
+ super(convert(value))
50
+ end
51
+
52
+ def unshift(*values)
53
+ super(*values.map { |v| convert(v) })
54
+ end
55
+
56
+ def to_mongo
57
+ ARRAY_CONVERTER.to_mongo(self)
58
+ end
59
+
60
+ def embedded_documents
61
+ select { |item| item.is_a?(EmbeddedDocument) }
62
+ end
63
+
64
+ class << self
65
+ def inspect
66
+ if type == Object
67
+ "Collection"
68
+ else
69
+ "Collection[#{type}]"
70
+ end
71
+ end
72
+
73
+ def [](type)
74
+ @collection_class_cache ||= {}
75
+ @collection_class_cache[type] ||= begin
76
+ collection = Class.new(Collection)
77
+ collection.type = type
78
+ collection
79
+ end
80
+ end
81
+
82
+ def from_mongo(array)
83
+ new(array.map { |i| instantiate(i) })
84
+ end
85
+
86
+ def converter
87
+ @converter ||= Types.converter_for(type)
88
+ end
89
+
90
+ private
91
+ def instantiate(item)
92
+ if item.is_a?(Hash) && item['_type']
93
+ item['_type'].constantize.from_mongo(item)
94
+ else
95
+ converter.from_mongo(item)
96
+ end
97
+ end
98
+ end
99
+
100
+ private
101
+ def convert(value)
102
+ converter.cast(value)
103
+ end
104
+
105
+ def converter
106
+ self.class.converter
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module MongoModel
4
+ class Configuration
5
+ def initialize(options)
6
+ @options = DEFAULTS.merge(options).stringify_keys
7
+ end
8
+
9
+ def host
10
+ @options['host']
11
+ end
12
+
13
+ def port
14
+ @options['port']
15
+ end
16
+
17
+ def database
18
+ @options['database']
19
+ end
20
+
21
+ def establish_connection
22
+ Mongo::Connection.new(host, port).db(database)
23
+ end
24
+
25
+ DEFAULTS = {
26
+ 'host' => 'localhost',
27
+ 'port' => 27017,
28
+ 'database' => 'mongomodel-default'
29
+ }
30
+
31
+ def self.defaults
32
+ new({})
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ class Boolean < TrueClass; end
2
+
3
+ class Symbol
4
+ [:lt, :lte, :gt, :gte, :ne, :in, :nin, :mod, :all, :size, :exists].each do |operator|
5
+ define_method(operator) { MongoModel::MongoOperator.new(self, operator) }
6
+ end
7
+
8
+ define_method(:asc) { MongoModel::MongoOrder::Clause.new(self, :ascending) }
9
+ define_method(:desc) { MongoModel::MongoOrder::Clause.new(self, :descending) }
10
+ end
@@ -0,0 +1,25 @@
1
+ module MongoModel
2
+ class DocumentNotFound < StandardError; end
3
+
4
+ class DocumentNotSaved < StandardError; end
5
+
6
+ # Raised by <tt>save!</tt> and <tt>create!</tt> when the document is invalid. Use the
7
+ # +document+ method to retrieve the document which did not validate.
8
+ # begin
9
+ # complex_operation_that_calls_save!_internally
10
+ # rescue MongoModel::DocumentInvalid => invalid
11
+ # puts invalid.document.errors
12
+ # end
13
+ class DocumentInvalid < DocumentNotSaved
14
+ attr_reader :document
15
+
16
+ def initialize(document)
17
+ @document = document
18
+
19
+ errors = @document.errors.full_messages.join(I18n.t('support.array.words_connector', :default => ', '))
20
+ super(I18n.t('mongomodel.errors.messages.document_invalid', :errors => errors))
21
+ end
22
+ end
23
+
24
+ class AssociationTypeMismatch < StandardError; end
25
+ end
@@ -0,0 +1,177 @@
1
+ module MongoModel
2
+ class MongoOptions
3
+ ValidKeys = [ :conditions, :select, :offset, :limit, :order ]
4
+
5
+ attr_reader :selector, :options
6
+
7
+ def initialize(model, options={})
8
+ options.assert_valid_keys(ValidKeys)
9
+
10
+ @model = model
11
+
12
+ @selector = extract_conditions(options)
13
+ @options = extract_options(options)
14
+
15
+ add_type_to_selector
16
+ end
17
+
18
+ def to_a
19
+ [selector, options]
20
+ end
21
+
22
+ private
23
+ def extract_conditions(options)
24
+ result = {}
25
+
26
+ (options[:conditions] || {}).each do |k, v|
27
+ if k.is_a?(MongoOperator)
28
+ key = k.field
29
+ value = k.to_mongo_selector(v)
30
+ else
31
+ key = k
32
+ value = v
33
+ end
34
+
35
+ property = @model.properties[key]
36
+
37
+ result[property ? property.as : key] = value
38
+ end
39
+
40
+ result
41
+ end
42
+
43
+ def extract_options(options)
44
+ result = {}
45
+
46
+ result[:fields] = options[:select] if options[:select]
47
+ result[:skip] = options[:offset] if options[:offset]
48
+ result[:limit] = options[:limit] if options[:limit]
49
+ result[:sort] = MongoOrder.parse(options[:order]).to_sort(@model) if options[:order]
50
+
51
+ result
52
+ end
53
+
54
+ def convert_order(order)
55
+ case order
56
+ when Array
57
+ order.map { |clause|
58
+ key, sort = clause.split(/ /)
59
+
60
+ property = @model.properties[key.to_sym]
61
+ sort = (sort =~ /desc/i) ? :descending : :ascending
62
+
63
+ [property ? property.as : key, sort]
64
+ } if order.size > 0
65
+ when String, Symbol
66
+ convert_order(order.to_s.split(/,/).map { |c| c.strip })
67
+ end
68
+ end
69
+
70
+ def add_type_to_selector
71
+ unless selector['_type'] || @model.superclass.abstract_class?
72
+ selector['_type'] = { '$in' => [@model.to_s] + @model.subclasses }
73
+ end
74
+ end
75
+ end
76
+
77
+ class MongoOrder
78
+ attr_reader :clauses
79
+
80
+ def initialize(*clauses)
81
+ @clauses = clauses
82
+ end
83
+
84
+ def to_s
85
+ clauses.map { |c| c.to_s }.join(', ')
86
+ end
87
+
88
+ def to_sort(model)
89
+ clauses.map { |c| c.to_sort(model.properties[c.field]) }
90
+ end
91
+
92
+ def ==(other)
93
+ other.is_a?(self.class) && clauses == other.clauses
94
+ end
95
+
96
+ def reverse
97
+ self.class.new(*clauses.map { |c| c.reverse })
98
+ end
99
+
100
+ def self.parse(order)
101
+ case order
102
+ when MongoOrder
103
+ order
104
+ when Clause
105
+ new(order)
106
+ when Symbol
107
+ new(Clause.new(order))
108
+ when String
109
+ new(*order.split(',').map { |c| Clause.parse(c) })
110
+ when Array
111
+ new(*order.map { |c| Clause.parse(c) })
112
+ end
113
+ end
114
+
115
+ class Clause
116
+ attr_reader :field, :order
117
+
118
+ def initialize(field, order=:ascending)
119
+ @field, @order = field.to_sym, order.to_sym
120
+ end
121
+
122
+ def to_s
123
+ "#{field} #{order}"
124
+ end
125
+
126
+ def to_sort(property)
127
+ [property ? property.as : field.to_s, order]
128
+ end
129
+
130
+ def reverse
131
+ self.class.new(field, order == :ascending ? :descending : :ascending)
132
+ end
133
+
134
+ def ==(other)
135
+ other.is_a?(self.class) && field == other.field && order == other.order
136
+ end
137
+
138
+ def self.parse(clause)
139
+ case clause
140
+ when Clause
141
+ clause
142
+ when String, Symbol
143
+ field, order = clause.to_s.strip.split(/ /)
144
+ new(field, order =~ /^desc/i ? :descending : :ascending)
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ class MongoOperator
151
+ attr_reader :field, :operator
152
+
153
+ def initialize(field, operator)
154
+ @field, @operator = field, operator
155
+ end
156
+
157
+ def to_mongo_selector(value)
158
+ { "$#{operator}" => value }
159
+ end
160
+
161
+ def inspect
162
+ "#{field.inspect}.#{operator}"
163
+ end
164
+
165
+ def ==(other)
166
+ other.is_a?(self.class) && field == other.field && operator == other.operator
167
+ end
168
+
169
+ def hash
170
+ field.hash ^ operator.hash
171
+ end
172
+
173
+ def eql?(other)
174
+ self == other
175
+ end
176
+ end
177
+ end