mongomodel 0.1

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