mongodb_model 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ module Mongo::Model::Assignment
2
+ class Dsl < BasicObject
3
+ def initialize
4
+ @attributes = {}
5
+ end
6
+
7
+ def self.const_missing name
8
+ # BasicObject doesn't have access to any constants like String, Symbol, ...
9
+ ::Object.const_get name
10
+ end
11
+
12
+ def to_h; attributes end
13
+
14
+ protected
15
+ attr_reader :attributes
16
+
17
+ def method_missing attribute_name, *args
18
+ attribute_name.must_be.a Symbol
19
+
20
+ args.size.must_be.in 1..2
21
+ if args.first.is_a? Class
22
+ type, mass_assignment = args
23
+ mass_assignment ||= false
24
+ type.must.respond_to :cast
25
+ else
26
+ type, mass_assignment = nil, args.first
27
+ end
28
+
29
+ attributes[attribute_name] = [type, mass_assignment]
30
+ end
31
+ end
32
+
33
+ def set attributes, options = {}
34
+ if rules = self.class._assign
35
+ force = options[:force]
36
+ attributes.each do |n, v|
37
+ n = n.to_sym
38
+ if rule = rules[n]
39
+ type, mass_assignment = rule
40
+ if mass_assignment or force
41
+ v = type.cast(v) if type
42
+ send "#{n}=", v
43
+ end
44
+ end
45
+ end
46
+ else
47
+ attributes.each{|n, v| send "#{n}=", v}
48
+ end
49
+ self
50
+ end
51
+
52
+ def set! attributes, options = {}
53
+ set attributes, options.merge(force: true)
54
+ end
55
+
56
+ module ClassMethods
57
+ inheritable_accessor :_assign, nil
58
+
59
+ def assign &block
60
+ dsl = ::Mongo::Model::Assignment::Dsl.new
61
+ dsl.instance_eval &block
62
+ self._assign = (_assign || {}).merge dsl.to_h
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ require 'json'
2
+ require 'yaml'
3
+
4
+ module Mongo::Model::AttributeConvertors
5
+ CONVERTORS = {
6
+ line: {
7
+ from_string: -> s {(s || "").split(',').collect{|s| s.strip}},
8
+ to_string: -> v {v.join(', ')}
9
+ },
10
+ column: {
11
+ from_string: -> s {(s || "").split("\n").collect{|s| s.strip}},
12
+ to_string: -> v {v.join("\n")}
13
+ },
14
+ yaml: {
15
+ from_string: -> s {YAML.load s rescue {}},
16
+ to_string: -> v {v.to_yaml.strip}
17
+ },
18
+ json: {
19
+ from_string: -> s {JSON.parse s rescue {}},
20
+ to_string: -> v {v.to_json.strip}
21
+ }
22
+ }
23
+
24
+ module ClassMethods
25
+ def available_as_string name, converter_name
26
+ converter = CONVERTORS[converter_name]
27
+ raise "unknown converter name :#{converter_name} for :#{name} field!" unless converter
28
+
29
+ from_string, to_string = converter[:from_string], converter[:to_string]
30
+ name_as_string = "#{name}_as_string".to_sym
31
+ define_method name_as_string do
32
+ _cache[name_as_string] ||= to_string.call(send(name))
33
+ end
34
+
35
+ define_method "#{name_as_string}=" do |value|
36
+ _cache.delete name_as_string
37
+ self.send "#{name}=", from_string.call(value)
38
+ end
39
+ end
40
+
41
+ def available_as_yaml name
42
+ raise "delimiter not specified for :#{name} field!" unless delimiter
43
+ method = "#{name}_as_string"
44
+ define_method method do
45
+ self.send(name).join(delimiter)
46
+ end
47
+ define_method "#{method}=" do |value|
48
+ value = (value || "").split(delimiter.strip).collect{|s| s.strip}
49
+ self.send "#{name}=", value
50
+ end
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,36 @@
1
+ module Mongo::Model::Callbacks
2
+ inherit RubyExt::Callbacks
3
+
4
+ def _run_callbacks type, method_name
5
+ if type == :before
6
+ run_before_callbacks method_name, method: method_name
7
+ elsif type == :after
8
+ run_after_callbacks method_name, method: method_name
9
+ else
10
+ raise "invalid callback type (#{type})!"
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ [:validate, :update, :save, :destroy].each do |method_name|
16
+ define_method "before_#{method_name}" do |*args, &block|
17
+ opt = args.extract_options!
18
+ if block
19
+ set_callback method_name, :before, opt, &block
20
+ else
21
+ opt[:terminator] = false unless opt.include? :terminator
22
+ args.each{|executor| set_callback method_name, :before, executor, opt}
23
+ end
24
+ end
25
+
26
+ define_method "after_#{method_name}" do |*args, &block|
27
+ opt = args.extract_options!
28
+ if block
29
+ set_callback method_name, :after, opt, &block
30
+ else
31
+ args.each{|executor| set_callback method_name, :after, executor, opt}
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ module Mongo::Model::Crud
2
+ def save opts = {}
3
+ with_collection opts do |collection, opts|
4
+ collection.save self, opts
5
+ end
6
+ end
7
+
8
+ def save! *args
9
+ save(*args) || raise(Mongo::Error, "can't save #{self.inspect}!")
10
+ end
11
+
12
+ def destroy opts = {}
13
+ with_collection opts do |collection, opts|
14
+ collection.destroy self, opts
15
+ end
16
+ end
17
+
18
+ def destroy! *args
19
+ destroy(*args) || raise(Mongo::Error, "can't destroy #{self.inspect}!")
20
+ end
21
+
22
+ module ClassMethods
23
+ def build attributes, opts = {}
24
+ self.new.set attributes, opts
25
+ end
26
+
27
+ def create attributes, opts = {}
28
+ o = build attributes, opts
29
+ o.save
30
+ o
31
+ end
32
+
33
+ def create! attributes, opts = {}
34
+ o = create attributes
35
+ raise(Mongo::Error, "can't create #{attributes.inspect}!") if o.new_record?
36
+ o
37
+ end
38
+
39
+ def destroy_all selector = {}, opts = {}
40
+ success = true
41
+ collection = opts[:collection] || self.collection
42
+ each(selector){|o| success = false unless o.destroy}
43
+ success
44
+ end
45
+
46
+ def destroy_all! selector = {}, opts = {}
47
+ destroy_all(selector, opts) || raise(Mongo::Error, "can't destroy #{selector.inspect}!")
48
+ end
49
+ end
50
+
51
+ protected
52
+ def with_collection opts, &block
53
+ opts = opts.clone
54
+ collection = opts.delete(:collection) || self.class.collection
55
+ block.call collection, opts
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ module Mongo::Model::Db
2
+ module ClassMethods
3
+ inheritable_accessor :_db, nil
4
+ def db= v
5
+ self._db = if v.is_a? ::Proc
6
+ v
7
+ elsif v.is_a? ::Symbol
8
+ -> {::Mongo::Model.connection.db v.to_s}
9
+ else
10
+ -> {v}
11
+ end
12
+ end
13
+
14
+ def db *args, &block
15
+ if block
16
+ self.db = block
17
+ elsif !args.empty?
18
+ args.size.must == 1
19
+ self.db = args.first
20
+ else
21
+ (_db && _db.call) || ::Mongo::Model.db
22
+ end
23
+ end
24
+
25
+ inheritable_accessor :_collection, nil
26
+ def collection= v
27
+ self._collection = if v.is_a? ::Proc
28
+ v
29
+ elsif v.is_a? ::Symbol
30
+ -> {db.collection v}
31
+ else
32
+ -> {v}
33
+ end
34
+ end
35
+
36
+ def collection *args, &block
37
+ if block
38
+ self.collection = block
39
+ elsif !args.empty?
40
+ args.size.must == 1
41
+ self.collection = args.first
42
+ else
43
+ (_collection && _collection.call) || db.collection(default_collection_name)
44
+ end
45
+ end
46
+
47
+ def default_collection_name
48
+ first_ancestor_class = ancestors.find{|a| a.is_a? Class} ||
49
+ raise("can't evaluate default collection name for #{self}!")
50
+ first_ancestor_class.alias.pluralize.underscore.to_sym
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ gem 'i18n', '>= 0.5'
2
+
3
+ if respond_to? :fake_gem
4
+ fake_gem 'validatable2'
5
+ fake_gem 'mongodb'
6
+ fake_gem 'ruby_ext'
7
+ end
@@ -0,0 +1,33 @@
1
+ module Mongo::Model::Misc
2
+ def update_timestamps
3
+ now = Time.now.utc
4
+ self.created_at ||= now
5
+ self.updated_at = now
6
+ end
7
+
8
+
9
+ def _cache
10
+ @_cache ||= {}
11
+ end
12
+ def _clear_cache
13
+ @_cache = {}
14
+ end
15
+
16
+
17
+ def dom_id
18
+ # new_record? ? "new_#{self.class.name.underscore}" : to_param
19
+ to_param
20
+ end
21
+
22
+ def to_param
23
+ (_id || '').to_s
24
+ end
25
+
26
+
27
+ module ClassMethods
28
+ def timestamps!
29
+ attr_accessor :created_at, :updated_at
30
+ before_save :update_timestamps
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ module Mongo::Model
2
+ attr_accessor :_id, :_class
3
+
4
+ def _id?; !!_id end
5
+ def new_record?; !_id end
6
+
7
+ class << self
8
+ attr_accessor :db, :connection
9
+ attr_required :db, :connection
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ module Mongo::Model::Query
2
+ module ClassMethods
3
+ include Mongo::DynamicFinders
4
+
5
+ def count selector = {}, opts = {}
6
+ collection.count selector, opts
7
+ end
8
+
9
+ def first selector = {}, opts = {}
10
+ collection.first selector, opts
11
+ end
12
+
13
+ def each selector = {}, opts = {}, &block
14
+ collection.each selector, opts, &block
15
+ end
16
+
17
+ def all selector = {}, opts = {}, &block
18
+ if block
19
+ each selector, opts, &block
20
+ else
21
+ list = []
22
+ each(selector, opts){|doc| list << doc}
23
+ list
24
+ end
25
+ end
26
+
27
+ def first! selector = {}, opts = {}
28
+ first(selector, opts) || raise(Mongo::NotFound, "document with selector #{selector} not found!")
29
+ end
30
+
31
+ def exists? selector = {}, opts = {}
32
+ count(selector, opts) > 0
33
+ end
34
+ alias :exist? :exists?
35
+ end
36
+ end
@@ -0,0 +1,99 @@
1
+ module Mongo::Model::Scope
2
+ class ScopeProxy < BasicObject
3
+ def initialize model, scope
4
+ @model, @scope = model, scope
5
+ end
6
+
7
+ def class
8
+ ::Mongo::Model::Scope::ScopeProxy
9
+ end
10
+
11
+ def reverse_merge! scope
12
+ @scope = scope.merge @scope
13
+ end
14
+
15
+ def inspect
16
+ "#<ScopeProxy:{#{scope.inspect}}>"
17
+ end
18
+ alias_method :to_s, :inspect
19
+
20
+ protected
21
+ attr_reader :model, :scope
22
+
23
+ def method_missing method, *args, &block
24
+ model.with_scope scope do
25
+ result = model.send method, *args, &block
26
+ result.reverse_merge! scope if result.class == ::Mongo::Model::Scope::ScopeProxy
27
+ result
28
+ end
29
+ end
30
+ end
31
+
32
+ module ClassMethods
33
+ def current_scope
34
+ scope, exclusive = Thread.current[:mongo_model_scope]
35
+ if exclusive
36
+ scope
37
+ elsif scope
38
+ default_scope.merge scope
39
+ else
40
+ default_scope
41
+ end
42
+ end
43
+
44
+ def with_exclusive_scope options = {}, &block
45
+ with_scope options, true, &block
46
+ end
47
+
48
+ def with_scope options = {}, exclusive = false, &block
49
+ previous_options, previous_exclusive = Thread.current[:mongo_model_scope]
50
+ raise "exclusive scope already applied!" if previous_exclusive
51
+
52
+ begin
53
+ options = previous_options.merge options if previous_options and !exclusive
54
+ Thread.current[:mongo_model_scope] = [options, exclusive]
55
+ return block.call
56
+ ensure
57
+ Thread.current[:mongo_model_scope] = [previous_options, false]
58
+ end
59
+ end
60
+
61
+ inheritable_accessor :_default_scope, -> {{}}
62
+ def default_scope *args, &block
63
+ if block
64
+ self._default_scope = block
65
+ elsif !args.empty?
66
+ args.size.must == 1
67
+ args.first.must_be.a Hash
68
+ scope = args.first
69
+ self._default_scope = -> {args.first}
70
+ else
71
+ _default_scope.call
72
+ end
73
+ end
74
+
75
+ def scope name, options = nil, &block
76
+ model = self
77
+ metaclass.define_method name do
78
+ scope = (block && block.call) || options
79
+ ScopeProxy.new model, scope
80
+ end
81
+ end
82
+
83
+
84
+ #
85
+ # finders
86
+ #
87
+ def count selector = {}, opts = {}
88
+ super current_scope.merge(selector), opts
89
+ end
90
+
91
+ def first selector = {}, opts = {}
92
+ super current_scope.merge(selector), opts
93
+ end
94
+
95
+ def each selector = {}, opts = {}, &block
96
+ super current_scope.merge(selector), opts, &block
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,12 @@
1
+ require 'mongodb/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,5 @@
1
+ module Mongo::Model::Validation
2
+ def _valid?
3
+ !(respond_to?(:errors) and errors and !errors.empty?)
4
+ end
5
+ end
@@ -0,0 +1,36 @@
1
+ require 'mongodb_model/gems'
2
+
3
+ require 'validatable'
4
+ require 'i18n'
5
+ require 'ruby_ext'
6
+ require 'mongodb/object'
7
+
8
+ module Mongo::Model; end
9
+
10
+ %w(
11
+ support/types
12
+
13
+ db
14
+ assignment
15
+ callbacks
16
+ validation
17
+ crud
18
+ query
19
+ scope
20
+ attribute_convertors
21
+ misc
22
+ model
23
+ ).each{|f| require "mongodb_model/#{f}"}
24
+
25
+ module Mongo
26
+ module Model
27
+ inherit Db, Assignment, Callbacks, Validation, Crud, Query, Scope, AttributeConvertors, Misc
28
+ end
29
+ end
30
+
31
+ Mongo.defaults.merge! \
32
+ symbolize: true,
33
+ convert_underscore_to_dollar: true,
34
+ batch_size: 50,
35
+ multi: true,
36
+ safe: true
data/readme.md CHANGED
@@ -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 'mongodb/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