mongodbmodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,12 @@
1
+ require 'mongo/object/spec'
2
+
3
+ rspec do
4
+ class << self
5
+ def with_mongo_model
6
+ with_mongo
7
+
8
+ before{Mongo::Model.db = mongo.db}
9
+ after{Mongo::Model.db = nil}
10
+ end
11
+ end
12
+ 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
@@ -0,0 +1,8 @@
1
+ gem 'i18n', '~> 0.5'
2
+
3
+ if respond_to? :fake_gem
4
+ fake_gem 'mongodb'
5
+ fake_gem 'file_model'
6
+ fake_gem 'validatable2'
7
+ fake_gem 'ruby_ext'
8
+ end
@@ -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