mongodbmodel 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/Rakefile +11 -0
- data/lib/mongo/model.rb +50 -0
- data/lib/mongo/model/assignment.rb +65 -0
- data/lib/mongo/model/attribute_convertors.rb +54 -0
- data/lib/mongo/model/callbacks.rb +26 -0
- data/lib/mongo/model/crud.rb +57 -0
- data/lib/mongo/model/db.rb +53 -0
- data/lib/mongo/model/file_model.rb +27 -0
- data/lib/mongo/model/misc.rb +44 -0
- data/lib/mongo/model/model.rb +13 -0
- data/lib/mongo/model/query.rb +46 -0
- data/lib/mongo/model/query_mixin.rb +45 -0
- data/lib/mongo/model/scope.rb +84 -0
- data/lib/mongo/model/spec.rb +12 -0
- data/lib/mongo/model/support/types.rb +110 -0
- data/lib/mongo/model/validation.rb +41 -0
- data/lib/mongo/model/validation/uniqueness_validator.rb +30 -0
- data/lib/mongodb_model/gems.rb +8 -0
- data/readme.md +72 -0
- data/spec/assignment_spec.rb +80 -0
- data/spec/associations_spec.rb +43 -0
- data/spec/attribute_convertors_spec.rb +73 -0
- data/spec/callbacks_spec.rb +36 -0
- data/spec/crud_spec.rb +151 -0
- data/spec/db_spec.rb +63 -0
- data/spec/file_model_spec.rb +23 -0
- data/spec/misc_spec.rb +67 -0
- data/spec/query_spec.rb +67 -0
- data/spec/scope_spec.rb +150 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/validation_spec.rb +127 -0
- metadata +75 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module Mongo::Model::QueryMixin
|
2
|
+
module ClassMethods
|
3
|
+
include Mongo::DynamicFinders
|
4
|
+
|
5
|
+
def count selector = {}, options = {}
|
6
|
+
collection.count selector, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def first selector = {}, options = {}
|
10
|
+
collection.first selector, options
|
11
|
+
end
|
12
|
+
|
13
|
+
def each selector = {}, options = {}, &block
|
14
|
+
collection.each selector, options, &block
|
15
|
+
end
|
16
|
+
|
17
|
+
def all selector = {}, options = {}, &block
|
18
|
+
if block
|
19
|
+
each selector, options, &block
|
20
|
+
else
|
21
|
+
list = []
|
22
|
+
each(selector, options){|doc| list << doc}
|
23
|
+
list
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def first! selector = {}, options = {}
|
28
|
+
first(selector, options) || raise(Mongo::NotFound, "document with selector #{selector} not found!")
|
29
|
+
end
|
30
|
+
|
31
|
+
def exists? selector = {}, options = {}
|
32
|
+
count(selector, options) > 0
|
33
|
+
end
|
34
|
+
alias :exist? :exists?
|
35
|
+
|
36
|
+
def query *args
|
37
|
+
if args.first.is_a? Mongo::Model::Query
|
38
|
+
args.first
|
39
|
+
else
|
40
|
+
selector, options = args.first.is_a?(::Array) ? args.first : args
|
41
|
+
Mongo::Model::Query.new self, (selector || {}), (options || {})
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Mongo::Model::Scope
|
2
|
+
module ClassMethods
|
3
|
+
def current_scope
|
4
|
+
scope, exclusive = Thread.current[:mongo_model_scope]
|
5
|
+
current = if exclusive
|
6
|
+
scope
|
7
|
+
elsif scope
|
8
|
+
default_scope ? default_scope.merge(scope) : scope
|
9
|
+
else
|
10
|
+
default_scope
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_exclusive_scope *args, &block
|
15
|
+
with_scope *(args << true), &block
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_scope *args, &block
|
19
|
+
if args.last.is_a?(TrueClass) or args.last.is_a?(FalseClass)
|
20
|
+
exclusive = args.pop
|
21
|
+
else
|
22
|
+
exclusive = false
|
23
|
+
end
|
24
|
+
|
25
|
+
scope = query *args
|
26
|
+
previous_scope, previous_exclusive = Thread.current[:mongo_model_scope]
|
27
|
+
raise "exclusive scope already applied!" if previous_exclusive
|
28
|
+
|
29
|
+
begin
|
30
|
+
scope = previous_scope.merge scope if !exclusive and previous_scope
|
31
|
+
Thread.current[:mongo_model_scope] = [scope, exclusive]
|
32
|
+
return block.call
|
33
|
+
ensure
|
34
|
+
Thread.current[:mongo_model_scope] = [previous_scope, false]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
inheritable_accessor :_default_scope, nil
|
39
|
+
def default_scope *args, &block
|
40
|
+
if block
|
41
|
+
self._default_scope = -> {query block.call}
|
42
|
+
elsif !args.empty?
|
43
|
+
self._default_scope = -> {query *args}
|
44
|
+
else
|
45
|
+
_default_scope && _default_scope.call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def scope name, *args, &block
|
50
|
+
model = self
|
51
|
+
metaclass.define_method name do
|
52
|
+
query (block && instance_eval(&block)) || args
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
#
|
58
|
+
# finders
|
59
|
+
#
|
60
|
+
def count selector = {}, options = {}
|
61
|
+
if current = current_scope
|
62
|
+
super current.selector.merge(selector), current.options.merge(options)
|
63
|
+
else
|
64
|
+
super selector, options
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def first selector = {}, options = {}
|
69
|
+
if current = current_scope
|
70
|
+
super current.selector.merge(selector), current.options.merge(options)
|
71
|
+
else
|
72
|
+
super selector, options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def each selector = {}, options = {}, &block
|
77
|
+
if current = current_scope
|
78
|
+
super current.selector.merge(selector), current.options.merge(options), &block
|
79
|
+
else
|
80
|
+
super selector, options, &block
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#
|
2
|
+
# Boolean
|
3
|
+
#
|
4
|
+
module Mongo::Model::BooleanType
|
5
|
+
Mapping = {
|
6
|
+
true => true,
|
7
|
+
'true' => true,
|
8
|
+
'TRUE' => true,
|
9
|
+
'True' => true,
|
10
|
+
't' => true,
|
11
|
+
'T' => true,
|
12
|
+
'1' => true,
|
13
|
+
1 => true,
|
14
|
+
1.0 => true,
|
15
|
+
false => false,
|
16
|
+
'false' => false,
|
17
|
+
'FALSE' => false,
|
18
|
+
'False' => false,
|
19
|
+
'f' => false,
|
20
|
+
'F' => false,
|
21
|
+
'0' => false,
|
22
|
+
0 => false,
|
23
|
+
0.0 => false,
|
24
|
+
nil => nil
|
25
|
+
}
|
26
|
+
|
27
|
+
def cast value
|
28
|
+
if value.is_a? Boolean
|
29
|
+
value
|
30
|
+
else
|
31
|
+
Mapping[value] || false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Boolean; end unless defined?(Boolean)
|
37
|
+
|
38
|
+
Boolean.extend Mongo::Model::BooleanType
|
39
|
+
|
40
|
+
|
41
|
+
#
|
42
|
+
# Date
|
43
|
+
#
|
44
|
+
require 'date'
|
45
|
+
Date.class_eval do
|
46
|
+
def self.cast value
|
47
|
+
if value.nil? || value == ''
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
date = value.is_a?(::Date) || value.is_a?(::Time) ? value : ::Date.parse(value.to_s)
|
51
|
+
date.to_date
|
52
|
+
end
|
53
|
+
rescue
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
#
|
60
|
+
# Float
|
61
|
+
#
|
62
|
+
Float.class_eval do
|
63
|
+
def self.cast value
|
64
|
+
value.nil? ? nil : value.to_f
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
#
|
70
|
+
# Integer
|
71
|
+
#
|
72
|
+
Integer.class_eval do
|
73
|
+
def self.cast value
|
74
|
+
value_to_i = value.to_i
|
75
|
+
if value_to_i == 0 && value != value_to_i
|
76
|
+
value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
|
77
|
+
else
|
78
|
+
value_to_i
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
#
|
85
|
+
# String
|
86
|
+
#
|
87
|
+
String.class_eval do
|
88
|
+
def self.cast value
|
89
|
+
value.nil? ? nil : value.to_s
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
#
|
95
|
+
# Time
|
96
|
+
#
|
97
|
+
Time.class_eval do
|
98
|
+
def self.cast value
|
99
|
+
if value.nil? || value == ''
|
100
|
+
nil
|
101
|
+
else
|
102
|
+
# time_class = ::Time.try(:zone).present? ? ::Time.zone : ::Time
|
103
|
+
# time = value.is_a?(::Time) ? value : time_class.parse(value.to_s)
|
104
|
+
# strip milliseconds as Ruby does micro and bson does milli and rounding rounded wrong
|
105
|
+
# at(time.to_i).utc if time
|
106
|
+
|
107
|
+
value.is_a?(::Time) ? value : Date.parse(value.to_s).to_time
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mongo::Model::Validation
|
2
|
+
def errors
|
3
|
+
@_errors ||= Validatable::Errors.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def run_validations
|
7
|
+
self.class.validations.each do |v|
|
8
|
+
if v.respond_to?(:validate)
|
9
|
+
v.validate self
|
10
|
+
elsif v.is_a? Proc
|
11
|
+
v.call self
|
12
|
+
else
|
13
|
+
send v
|
14
|
+
end
|
15
|
+
end
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
include ::Validatable::Macros
|
21
|
+
|
22
|
+
inheritable_accessor :validations, []
|
23
|
+
|
24
|
+
def validates_uniqueness_of *args
|
25
|
+
add_validations(args, Mongo::Model::UniquenessValidator)
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate validation
|
29
|
+
validations << validation
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
def add_validations(args, klass)
|
34
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
35
|
+
args.each do |attribute|
|
36
|
+
new_validation = klass.new self, attribute, options
|
37
|
+
validate new_validation
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Mongo::Model::UniquenessValidator < Validatable::ValidationBase
|
2
|
+
attr_accessor :scope, :case_sensitive
|
3
|
+
|
4
|
+
def initialize(klass, attribute, options={})
|
5
|
+
super
|
6
|
+
self.case_sensitive = false if case_sensitive == nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?(instance)
|
10
|
+
conditions = {}
|
11
|
+
|
12
|
+
conditions[scope] = instance.send scope if scope
|
13
|
+
|
14
|
+
value = instance.send attribute
|
15
|
+
if case_sensitive
|
16
|
+
conditions[attribute] = value
|
17
|
+
else
|
18
|
+
conditions[attribute] = /^#{Regexp.escape(value.to_s)}$/i
|
19
|
+
end
|
20
|
+
|
21
|
+
# Make sure we're not including the current document in the query
|
22
|
+
conditions[:_id] = {_ne: instance._id} if instance._id
|
23
|
+
|
24
|
+
!klass.exists?(conditions)
|
25
|
+
end
|
26
|
+
|
27
|
+
def message(instance)
|
28
|
+
super || "#{attribute} must be unique!"
|
29
|
+
end
|
30
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
Object Model for MongoDB (callbacks, validations, mass-assignment, finders, ...).
|
2
|
+
|
3
|
+
- The same API for pure driver and Models.
|
4
|
+
- Minimum extra abstractions, trying to keep things as close to the MongoDB semantic as possible.
|
5
|
+
- Schema-less, dynamic (with ability to specify types for mass-assignment).
|
6
|
+
- Models can be saved to any collection.
|
7
|
+
- Full support for embedded objects (validations, callbacks, ...).
|
8
|
+
- Scope, default_scope
|
9
|
+
- Doesn't try to mimic ActiveRecord, MongoDB is differrent and this tool designed to get most of it.
|
10
|
+
- Very small, see [code stats][code_stats].
|
11
|
+
|
12
|
+
Other ODM usually try to cover simple but non-standard API of MongoDB behind complex ORM-like abstractions. This tool **exposes simplicity and power of MongoDB and leverages it's differences**.
|
13
|
+
|
14
|
+
``` ruby
|
15
|
+
# Connecting to MongoDB.
|
16
|
+
require 'mongo/model'
|
17
|
+
Mongo.defaults.merge! symbolize: true, multi: true, safe: true
|
18
|
+
connection = Mongo::Connection.new
|
19
|
+
db = connection.db 'default_test'
|
20
|
+
db.units.drop
|
21
|
+
Mongo::Model.db = db
|
22
|
+
|
23
|
+
# Let's define the game unit.
|
24
|
+
class Unit
|
25
|
+
inherit Mongo::Model
|
26
|
+
collection :units
|
27
|
+
|
28
|
+
attr_accessor :name, :status, :stats
|
29
|
+
|
30
|
+
scope :alive, status: 'alive'
|
31
|
+
|
32
|
+
class Stats
|
33
|
+
inherit Mongo::Model
|
34
|
+
attr_accessor :attack, :life, :shield
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create.
|
39
|
+
zeratul = Unit.build(name: 'Zeratul', status: 'alive', stats: Unit::Stats.build(attack: 85, life: 300, shield: 100))
|
40
|
+
tassadar = Unit.build(name: 'Tassadar', status: 'dead', stats: Unit::Stats.build(attack: 0, life: 80, shield: 300))
|
41
|
+
|
42
|
+
zeratul.save
|
43
|
+
tassadar.save
|
44
|
+
|
45
|
+
# Udate (we made error - mistakenly set Tassadar's attack as zero, let's fix it).
|
46
|
+
tassadar.stats.attack = 20
|
47
|
+
tassadar.save
|
48
|
+
|
49
|
+
# Querying first & all, there's also :each, the same as :all.
|
50
|
+
Unit.first name: 'Zeratul' # => zeratul
|
51
|
+
Unit.all name: 'Zeratul' # => [zeratul]
|
52
|
+
Unit.all name: 'Zeratul' do |unit|
|
53
|
+
unit # => zeratul
|
54
|
+
end
|
55
|
+
|
56
|
+
# Simple finders (bang versions also availiable).
|
57
|
+
Unit.by_name 'Zeratul' # => zeratul
|
58
|
+
Unit.first_by_name 'Zeratul' # => zeratul
|
59
|
+
Unit.all_by_name 'Zeratul' # => [zeratul]
|
60
|
+
|
61
|
+
# Scopes.
|
62
|
+
Unit.alive.count # => 1
|
63
|
+
Unit.alive.first # => zeratul
|
64
|
+
|
65
|
+
# Callbacks & callbacks on embedded models.
|
66
|
+
|
67
|
+
# Validations.
|
68
|
+
|
69
|
+
# Save model to any collection.
|
70
|
+
```
|
71
|
+
|
72
|
+
Source: examples/model.rb
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Model callbacks' do
|
4
|
+
with_mongo_model
|
5
|
+
|
6
|
+
after{remove_constants :User, :Writer}
|
7
|
+
|
8
|
+
it "should update attributes" do
|
9
|
+
class User
|
10
|
+
inherit Mongo::Model
|
11
|
+
|
12
|
+
attr_accessor :name, :has_mail, :age, :banned
|
13
|
+
end
|
14
|
+
|
15
|
+
u = User.new
|
16
|
+
u.set name: 'Alex', has_mail: '1', age: '31', banned: '0'
|
17
|
+
[u.name, u.has_mail, u.age, u.banned].should == ['Alex', '1', '31', '0']
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should update only specified attributes" do
|
21
|
+
class User
|
22
|
+
inherit Mongo::Model
|
23
|
+
|
24
|
+
attr_accessor :name, :has_mail, :age, :position, :banned
|
25
|
+
|
26
|
+
assign do
|
27
|
+
name String, true
|
28
|
+
has_mail Boolean, true
|
29
|
+
age Integer, true
|
30
|
+
position true
|
31
|
+
banned Boolean
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
u = User.new
|
36
|
+
u.set name: 'Alex', has_mail: '1', age: '31', position: [11, 34] ,banned: '0'
|
37
|
+
[u.name, u.has_mail, u.age, u.position, u.banned].should == ['Alex', true, 31, [11, 34], nil]
|
38
|
+
|
39
|
+
# should allow to forcefully cast and update any attribute
|
40
|
+
u.set! banned: '0'
|
41
|
+
u.banned.should == false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should inherit assignment rules" do
|
45
|
+
class User
|
46
|
+
inherit Mongo::Model
|
47
|
+
|
48
|
+
attr_accessor :age
|
49
|
+
|
50
|
+
assign do
|
51
|
+
age Integer, true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Writer < User
|
56
|
+
attr_accessor :posts
|
57
|
+
|
58
|
+
assign do
|
59
|
+
posts Integer, true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
u = Writer.new
|
64
|
+
u.set age: '20', posts: '12'
|
65
|
+
[u.age, u.posts].should == [20, 12]
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'casting smoke test' do
|
69
|
+
[
|
70
|
+
Boolean, '1', true,
|
71
|
+
Date, '2011-08-23', Date.parse('2011-08-23'),
|
72
|
+
Float, '1.2', 1.2,
|
73
|
+
Integer, '10', 10,
|
74
|
+
String, 'Hi', 'Hi',
|
75
|
+
Time, '2011-08-23', Date.parse('2011-08-23').to_time
|
76
|
+
].each_slice 3 do |type, raw, expected|
|
77
|
+
type.cast(raw).should == expected
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|