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.
@@ -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