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.
- data/LICENSE +22 -0
- data/README.md +34 -0
- data/Rakefile +47 -0
- data/bin/console +45 -0
- data/lib/mongomodel.rb +92 -0
- data/lib/mongomodel/attributes/mongo.rb +40 -0
- data/lib/mongomodel/attributes/store.rb +30 -0
- data/lib/mongomodel/attributes/typecasting.rb +51 -0
- data/lib/mongomodel/concerns/abstract_class.rb +17 -0
- data/lib/mongomodel/concerns/activemodel.rb +11 -0
- data/lib/mongomodel/concerns/associations.rb +103 -0
- data/lib/mongomodel/concerns/associations/base/association.rb +33 -0
- data/lib/mongomodel/concerns/associations/base/definition.rb +56 -0
- data/lib/mongomodel/concerns/associations/base/proxy.rb +58 -0
- data/lib/mongomodel/concerns/associations/belongs_to.rb +68 -0
- data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +159 -0
- data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +175 -0
- data/lib/mongomodel/concerns/attribute_methods.rb +55 -0
- data/lib/mongomodel/concerns/attribute_methods/before_type_cast.rb +29 -0
- data/lib/mongomodel/concerns/attribute_methods/dirty.rb +35 -0
- data/lib/mongomodel/concerns/attribute_methods/protected.rb +127 -0
- data/lib/mongomodel/concerns/attribute_methods/query.rb +22 -0
- data/lib/mongomodel/concerns/attribute_methods/read.rb +29 -0
- data/lib/mongomodel/concerns/attribute_methods/write.rb +29 -0
- data/lib/mongomodel/concerns/attributes.rb +85 -0
- data/lib/mongomodel/concerns/callbacks.rb +294 -0
- data/lib/mongomodel/concerns/logging.rb +15 -0
- data/lib/mongomodel/concerns/pretty_inspect.rb +29 -0
- data/lib/mongomodel/concerns/properties.rb +69 -0
- data/lib/mongomodel/concerns/record_status.rb +42 -0
- data/lib/mongomodel/concerns/timestamps.rb +32 -0
- data/lib/mongomodel/concerns/validations.rb +38 -0
- data/lib/mongomodel/concerns/validations/associated.rb +46 -0
- data/lib/mongomodel/document.rb +20 -0
- data/lib/mongomodel/document/callbacks.rb +46 -0
- data/lib/mongomodel/document/dynamic_finders.rb +88 -0
- data/lib/mongomodel/document/finders.rb +82 -0
- data/lib/mongomodel/document/indexes.rb +91 -0
- data/lib/mongomodel/document/optimistic_locking.rb +48 -0
- data/lib/mongomodel/document/persistence.rb +143 -0
- data/lib/mongomodel/document/scopes.rb +161 -0
- data/lib/mongomodel/document/validations.rb +68 -0
- data/lib/mongomodel/document/validations/uniqueness.rb +78 -0
- data/lib/mongomodel/embedded_document.rb +42 -0
- data/lib/mongomodel/locale/en.yml +55 -0
- data/lib/mongomodel/support/collection.rb +109 -0
- data/lib/mongomodel/support/configuration.rb +35 -0
- data/lib/mongomodel/support/core_extensions.rb +10 -0
- data/lib/mongomodel/support/exceptions.rb +25 -0
- data/lib/mongomodel/support/mongo_options.rb +177 -0
- data/lib/mongomodel/support/types.rb +35 -0
- data/lib/mongomodel/support/types/array.rb +11 -0
- data/lib/mongomodel/support/types/boolean.rb +25 -0
- data/lib/mongomodel/support/types/custom.rb +38 -0
- data/lib/mongomodel/support/types/date.rb +20 -0
- data/lib/mongomodel/support/types/float.rb +13 -0
- data/lib/mongomodel/support/types/hash.rb +18 -0
- data/lib/mongomodel/support/types/integer.rb +13 -0
- data/lib/mongomodel/support/types/object.rb +21 -0
- data/lib/mongomodel/support/types/string.rb +9 -0
- data/lib/mongomodel/support/types/symbol.rb +9 -0
- data/lib/mongomodel/support/types/time.rb +12 -0
- data/lib/mongomodel/version.rb +3 -0
- data/spec/mongomodel/attributes/store_spec.rb +273 -0
- data/spec/mongomodel/concerns/activemodel_spec.rb +61 -0
- data/spec/mongomodel/concerns/associations/belongs_to_spec.rb +153 -0
- data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +165 -0
- data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +192 -0
- data/spec/mongomodel/concerns/attribute_methods/before_type_cast_spec.rb +46 -0
- data/spec/mongomodel/concerns/attribute_methods/dirty_spec.rb +131 -0
- data/spec/mongomodel/concerns/attribute_methods/protected_spec.rb +86 -0
- data/spec/mongomodel/concerns/attribute_methods/query_spec.rb +27 -0
- data/spec/mongomodel/concerns/attribute_methods/read_spec.rb +52 -0
- data/spec/mongomodel/concerns/attribute_methods/write_spec.rb +43 -0
- data/spec/mongomodel/concerns/attributes_spec.rb +152 -0
- data/spec/mongomodel/concerns/callbacks_spec.rb +90 -0
- data/spec/mongomodel/concerns/logging_spec.rb +20 -0
- data/spec/mongomodel/concerns/pretty_inspect_spec.rb +68 -0
- data/spec/mongomodel/concerns/properties_spec.rb +29 -0
- data/spec/mongomodel/concerns/timestamps_spec.rb +170 -0
- data/spec/mongomodel/concerns/validations_spec.rb +159 -0
- data/spec/mongomodel/document/callbacks_spec.rb +80 -0
- data/spec/mongomodel/document/dynamic_finders_spec.rb +183 -0
- data/spec/mongomodel/document/finders_spec.rb +231 -0
- data/spec/mongomodel/document/indexes_spec.rb +121 -0
- data/spec/mongomodel/document/optimistic_locking_spec.rb +57 -0
- data/spec/mongomodel/document/persistence_spec.rb +319 -0
- data/spec/mongomodel/document/scopes_spec.rb +204 -0
- data/spec/mongomodel/document/validations/uniqueness_spec.rb +217 -0
- data/spec/mongomodel/document/validations_spec.rb +132 -0
- data/spec/mongomodel/document_spec.rb +74 -0
- data/spec/mongomodel/embedded_document_spec.rb +66 -0
- data/spec/mongomodel/mongomodel_spec.rb +33 -0
- data/spec/mongomodel/support/collection_spec.rb +248 -0
- data/spec/mongomodel/support/mongo_options_spec.rb +295 -0
- data/spec/mongomodel/support/property_spec.rb +83 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/specdoc.opts +6 -0
- data/spec/support/callbacks.rb +44 -0
- data/spec/support/helpers/define_class.rb +24 -0
- data/spec/support/helpers/specs_for.rb +11 -0
- data/spec/support/matchers/be_a_subclass_of.rb +5 -0
- data/spec/support/matchers/respond_to_boolean.rb +17 -0
- data/spec/support/matchers/run_callbacks.rb +20 -0
- data/spec/support/models.rb +23 -0
- data/spec/support/time.rb +6 -0
- 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
|