jnunemaker-mongomapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ class Boolean; end
2
+
3
+ module MongoMapper
4
+ class Key
5
+ # DateTime and Date are currently not supported by mongo's bson so just use Time
6
+ NativeTypes = [String, Float, Time, Integer, Boolean, Array, Hash]
7
+
8
+ attr_accessor :name, :type, :options, :default_value
9
+
10
+ def initialize(name, type, options={})
11
+ @name, @type = name.to_s, type
12
+ self.options = options.symbolize_keys
13
+ self.default_value = options.delete(:default)
14
+ end
15
+
16
+ def ==(other)
17
+ @name == other.name && @type == other.type
18
+ end
19
+
20
+ def set(value)
21
+ typecast(value)
22
+ end
23
+
24
+ def native?
25
+ @native ||= NativeTypes.include?(type)
26
+ end
27
+
28
+ def embedded_document?
29
+ type.ancestors.include?(EmbeddedDocument) && !type.ancestors.include?(Document)
30
+ end
31
+
32
+ def get(value)
33
+ return default_value if value.nil? && !default_value.nil?
34
+ if type == Array
35
+ value || []
36
+ elsif type == Hash
37
+ HashWithIndifferentAccess.new(value || {})
38
+ else
39
+ value
40
+ end
41
+ end
42
+
43
+ private
44
+ def typecast(value)
45
+ return HashWithIndifferentAccess.new(value) if value.is_a?(Hash) && type == Hash
46
+ return value if value.kind_of?(type) || value.nil?
47
+ begin
48
+ if type == String then value.to_s
49
+ elsif type == Float then value.to_f
50
+ elsif type == Array then value.to_a
51
+ elsif type == Time then Time.parse(value.to_s)
52
+ #elsif type == Date then Date.parse(value.to_s)
53
+ elsif type == Boolean then ['true', 't', '1'].include?(value.to_s.downcase)
54
+ elsif type == Integer
55
+ # ganked from datamapper
56
+ value_to_i = value.to_i
57
+ if value_to_i == 0 && value != '0'
58
+ value_to_s = value.to_s
59
+ begin
60
+ Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
61
+ rescue ArgumentError
62
+ nil
63
+ end
64
+ else
65
+ value_to_i
66
+ end
67
+ elsif embedded_document?
68
+ typecast_embedded_document(value)
69
+ else
70
+ value
71
+ end
72
+ rescue
73
+ value
74
+ end
75
+ end
76
+
77
+ def typecast_embedded_document(value)
78
+ value.is_a?(type) ? value : type.new(value)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,20 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ def self.included(model)
4
+ model.class_eval do
5
+ alias_method :new_record?, :new?
6
+ extend ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ def column_names
12
+ keys.keys
13
+ end
14
+ end
15
+
16
+ def to_param
17
+ id
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module MongoMapper
2
+ module SaveWithValidation
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :valid?, :callbacks
6
+ alias_method_chain :save, :validation
7
+ end
8
+ end
9
+
10
+ def save!
11
+ save_with_validation || raise(DocumentNotValid.new(self))
12
+ end
13
+
14
+ private
15
+ def save_with_validation
16
+ new? ? run_callbacks(:before_validation_on_create) :
17
+ run_callbacks(:before_validation_on_update)
18
+
19
+ valid? ? save_without_validation : false
20
+ end
21
+
22
+ def valid_with_callbacks?
23
+ run_callbacks(:before_validation)
24
+ run_callbacks(:after_validation) if valid_without_callbacks?
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_support/json'
2
+
3
+ module MongoMapper #:nodoc:
4
+ module Serialization
5
+ class Serializer #:nodoc:
6
+ attr_reader :options
7
+
8
+ def initialize(record, options = {})
9
+ @record, @options = record, options.dup
10
+ end
11
+
12
+ def serializable_key_names
13
+ key_names = @record.send :defined_key_names
14
+
15
+ if options[:only]
16
+ options.delete(:except)
17
+ key_names = key_names & Array(options[:only]).collect { |n| n.to_s }
18
+ else
19
+ options[:except] = Array(options[:except])
20
+ key_names = key_names - options[:except].collect { |n| n.to_s }
21
+ end
22
+
23
+ key_names
24
+ end
25
+
26
+ def serializable_method_names
27
+ Array(options[:methods]).inject([]) do |method_attributes, name|
28
+ method_attributes << name if @record.respond_to?(name.to_s)
29
+ method_attributes
30
+ end
31
+ end
32
+
33
+ def serializable_names
34
+ serializable_key_names + serializable_method_names
35
+ end
36
+
37
+ def serializable_record
38
+ returning(serializable_record = {}) do
39
+ serializable_names.each { |name| serializable_record[name] = @record.send(name) }
40
+ end
41
+ end
42
+
43
+ def serialize
44
+ # overwrite to implement
45
+ end
46
+
47
+ def to_s(&block)
48
+ serialize(&block)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ dir = Pathname(__FILE__).dirname.expand_path + 'serializers'
55
+ require dir + 'json_serializer'
@@ -0,0 +1,77 @@
1
+ module MongoMapper #:nodoc:
2
+ module Serialization
3
+ def self.included(base)
4
+ base.cattr_accessor :include_root_in_json, :instance_writer => false
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # Returns a JSON string representing the model. Some configuration is
9
+ # available through +options+.
10
+ #
11
+ # The option <tt>include_root_in_json</tt> controls the top-level behavior of
12
+ # to_json. When it is <tt>true</tt>, to_json will emit a single root node named
13
+ # after the object's type. For example:
14
+ #
15
+ # konata = User.find(1)
16
+ # User.include_root_in_json = true
17
+ # konata.to_json
18
+ # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
19
+ # "created_at": "2006/08/01", "awesome": true} }
20
+ #
21
+ # User.include_root_in_json = false
22
+ # konata.to_json
23
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
24
+ # "created_at": "2006/08/01", "awesome": true}
25
+ #
26
+ # The remainder of the examples in this section assume include_root_in_json is set to
27
+ # <tt>false</tt>.
28
+ #
29
+ # Without any +options+, the returned JSON string will include all
30
+ # the model's attributes. For example:
31
+ #
32
+ # konata = User.find(1)
33
+ # konata.to_json
34
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
35
+ # "created_at": "2006/08/01", "awesome": true}
36
+ #
37
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
38
+ # included, and work similar to the +attributes+ method. For example:
39
+ #
40
+ # konata.to_json(:only => [ :id, :name ])
41
+ # # => {"id": 1, "name": "Konata Izumi"}
42
+ #
43
+ # konata.to_json(:except => [ :id, :created_at, :age ])
44
+ # # => {"name": "Konata Izumi", "awesome": true}
45
+ #
46
+ # To include any methods on the model, use <tt>:methods</tt>.
47
+ #
48
+ # konata.to_json(:methods => :permalink)
49
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
50
+ # "created_at": "2006/08/01", "awesome": true,
51
+ # "permalink": "1-konata-izumi"}
52
+ def to_json(options = {})
53
+ if include_root_in_json
54
+ "{#{self.class.json_class_name}: #{JsonSerializer.new(self, options).to_s}}"
55
+ else
56
+ JsonSerializer.new(self, options).to_s
57
+ end
58
+ end
59
+
60
+ def from_json(json)
61
+ self.attributes = ActiveSupport::JSON.decode(json)
62
+ self
63
+ end
64
+
65
+ class JsonSerializer < MongoMapper::Serialization::Serializer #:nodoc:
66
+ def serialize
67
+ serializable_record.to_json
68
+ end
69
+ end
70
+
71
+ module ClassMethods
72
+ def json_class_name
73
+ @json_class_name ||= name.demodulize.underscore.inspect
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,46 @@
1
+ require 'pathname'
2
+ require 'rubygems'
3
+
4
+ gem 'activesupport'
5
+ gem 'mongodb-mongo'
6
+ gem 'jnunemaker-validatable'
7
+
8
+ require 'activesupport'
9
+ require 'mongo'
10
+ require 'validatable'
11
+
12
+ dir = Pathname(__FILE__).dirname.expand_path + 'mongomapper'
13
+
14
+ require dir + 'key'
15
+ require dir + 'finder_options'
16
+ require dir + 'rails_compatibility'
17
+ require dir + 'save_with_validation'
18
+ require dir + 'serialization'
19
+ require dir + 'embedded_document'
20
+ require dir + 'document'
21
+
22
+ module MongoMapper
23
+ class DocumentNotFound < StandardError; end
24
+ class DocumentNotValid < StandardError
25
+ def initialize(document)
26
+ @document = document
27
+ super("Validation failed: #{@document.errors.full_messages.join(", ")}")
28
+ end
29
+ end
30
+
31
+ def self.connection
32
+ @@connection ||= XGen::Mongo::Driver::Mongo.new
33
+ end
34
+
35
+ def self.connection=(new_connection)
36
+ @@connection = new_connection
37
+ end
38
+
39
+ def self.database=(name)
40
+ @@database = MongoMapper.connection.db(name)
41
+ end
42
+
43
+ def self.database
44
+ @@database
45
+ end
46
+ end
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+
3
+ class JsonSerializationTest < Test::Unit::TestCase
4
+ class Contact
5
+ include MongoMapper::EmbeddedDocument
6
+ key :name, String
7
+ key :age, Integer
8
+ key :created_at, Time
9
+ key :awesome, Boolean
10
+ key :preferences, Hash
11
+ end
12
+
13
+ def setup
14
+ Contact.include_root_in_json = false
15
+ @contact = Contact.new(
16
+ :name => 'Konata Izumi',
17
+ :age => 16,
18
+ :created_at => Time.utc(2006, 8, 1),
19
+ :awesome => true,
20
+ :preferences => { :shows => 'anime' }
21
+ )
22
+ end
23
+
24
+ should "include demodulized root" do
25
+ Contact.include_root_in_json = true
26
+ assert_match %r{^\{"contact": \{}, @contact.to_json
27
+ end
28
+
29
+ should "encode all encodable attributes" do
30
+ json = @contact.to_json
31
+
32
+ assert_match %r{"name": "Konata Izumi"}, json
33
+ assert_match %r{"age": 16}, json
34
+ assert json.include?(%("created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
35
+ assert_match %r{"awesome": true}, json
36
+ assert_match %r{"preferences": \{"shows": "anime"\}}, json
37
+ end
38
+
39
+ should "allow attribute filtering with only" do
40
+ json = @contact.to_json(:only => [:name, :age])
41
+
42
+ assert_match %r{"name": "Konata Izumi"}, json
43
+ assert_match %r{"age": 16}, json
44
+ assert_no_match %r{"awesome"}, json
45
+ assert_no_match %r{"created_at"}, json
46
+ assert_no_match %r{"preferences"}, json
47
+ end
48
+
49
+ should "allow attribute filtering with except" do
50
+ json = @contact.to_json(:except => [:name, :age])
51
+
52
+ assert_no_match %r{"name"}, json
53
+ assert_no_match %r{"age"}, json
54
+ assert_match %r{"awesome"}, json
55
+ assert_match %r{"created_at"}, json
56
+ assert_match %r{"preferences"}, json
57
+ end
58
+
59
+ context "including methods" do
60
+ setup do
61
+ def @contact.label; "Has cheezburger"; end
62
+ def @contact.favorite_quote; "Constraints are liberating"; end
63
+ end
64
+
65
+ should "include single method" do
66
+ # Single method.
67
+ assert_match %r{"label": "Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label)
68
+ end
69
+
70
+ should "include multiple methods" do
71
+ json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote])
72
+ assert_match %r{"label": "Has cheezburger"}, json
73
+ assert_match %r{"favorite_quote": "Constraints are liberating"}, json
74
+ end
75
+ end
76
+
77
+ context "array of records" do
78
+ setup do
79
+ @contacts = [
80
+ Contact.new(:name => 'David', :age => 39),
81
+ Contact.new(:name => 'Mary', :age => 14)
82
+ ]
83
+ end
84
+
85
+ should "allow attribute filtering with only" do
86
+ assert_equal %([{"name": "David"}, {"name": "Mary"}]), @contacts.to_json(:only => :name)
87
+ end
88
+
89
+ should "allow attribute filtering with except" do
90
+ json = @contacts.to_json(:except => [:name, :preferences, :awesome, :created_at])
91
+ assert_equal %([{"age": 39}, {"age": 14}]), json
92
+ end
93
+ end
94
+
95
+ should "allow options for hash of records" do
96
+ contacts = {
97
+ 1 => Contact.new(:name => 'David', :age => 39),
98
+ 2 => Contact.new(:name => 'Mary', :age => 14)
99
+ }
100
+
101
+ assert_equal %({"1": {"name": "David"}}), contacts.to_json(:only => [1, :name])
102
+ end
103
+
104
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+
3
+ class Address
4
+ include MongoMapper::EmbeddedDocument
5
+
6
+ key :address, String
7
+ key :city, String
8
+ key :state, String
9
+ key :zip, Integer
10
+ end
11
+
12
+ class AssociationsTest < Test::Unit::TestCase
13
+ def setup
14
+ @document = Class.new do
15
+ include MongoMapper::Document
16
+ end
17
+ end
18
+
19
+ context "Many embedded documents" do
20
+ setup do
21
+ @document.class_eval do
22
+ many :addresses
23
+ end
24
+ end
25
+
26
+ should "default reader to empty array" do
27
+ instance = @document.new
28
+ instance.addresses.should == []
29
+ end
30
+
31
+ should "allow adding to association like it was an array" do
32
+ instance = @document.new
33
+ instance.addresses << Address.new
34
+ instance.addresses.push Address.new
35
+ instance.addresses.size.should == 2
36
+ end
37
+
38
+ should "be embedded in document on save" do
39
+ sb = Address.new(:city => 'South Bend', :state => 'IN')
40
+ chi = Address.new(:city => 'Chicago', :state => 'IL')
41
+ instance = @document.new
42
+ instance.addresses << sb
43
+ instance.addresses << chi
44
+ instance.save
45
+
46
+ from_db = @document.find(instance.id)
47
+ from_db.addresses.size.should == 2
48
+ from_db.addresses[0].should == sb
49
+ from_db.addresses[1].should == chi
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,86 @@
1
+ require 'test_helper'
2
+
3
+ include ActiveSupport::Callbacks
4
+
5
+ class CallbacksTest < Test::Unit::TestCase
6
+ context "Defining and running callbacks" do
7
+ setup do
8
+ @document = Class.new do
9
+ include MongoMapper::Document
10
+
11
+ key :name, String
12
+
13
+ [ :before_validation_on_create, :before_validation_on_update,
14
+ :before_validation, :after_validation,
15
+ :before_create, :after_create,
16
+ :before_update, :after_update,
17
+ :before_save, :after_save,
18
+ :before_destroy, :after_destroy].each do |callback|
19
+ callback_method = "#{callback}_callback"
20
+ send(callback, callback_method)
21
+ define_method(callback_method) do
22
+ history << callback.to_sym
23
+ end
24
+ end
25
+
26
+ def history
27
+ @history ||= []
28
+ end
29
+
30
+ def clear_history
31
+ @history = nil
32
+ end
33
+ end
34
+ @document.collection.clear
35
+ end
36
+
37
+ should "get the order right for creating documents" do
38
+ doc = @document.create(:name => 'John Nunemaker')
39
+ doc.history.should == [:before_validation_on_create, :before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save]
40
+ end
41
+
42
+ should "get the order right for updating documents" do
43
+ doc = @document.create(:name => 'John Nunemaker')
44
+ doc.clear_history
45
+ doc.name = 'John'
46
+ doc.save
47
+ doc.history.should == [:before_validation_on_update, :before_validation, :after_validation, :before_save, :before_update, :after_update, :after_save]
48
+ end
49
+
50
+ should "work for before and after validation" do
51
+ doc = @document.new(:name => 'John Nunemaker')
52
+ doc.valid?
53
+ doc.history.should include(:before_validation)
54
+ doc.history.should include(:after_validation)
55
+ end
56
+
57
+ should "work for before and after create" do
58
+ doc = @document.create(:name => 'John Nunemaker')
59
+ doc.history.should include(:before_create)
60
+ doc.history.should include(:after_create)
61
+ end
62
+
63
+ should "work for before and after update" do
64
+ doc = @document.create(:name => 'John Nunemaker')
65
+ doc.name = 'John Doe'
66
+ doc.save
67
+ doc.history.should include(:before_update)
68
+ doc.history.should include(:after_update)
69
+ end
70
+
71
+ should "work for before and after save" do
72
+ doc = @document.new
73
+ doc.name = 'John Doe'
74
+ doc.save
75
+ doc.history.should include(:before_save)
76
+ doc.history.should include(:after_save)
77
+ end
78
+
79
+ should "work for before and after destroy" do
80
+ doc = @document.create(:name => 'John Nunemaker')
81
+ doc.destroy
82
+ doc.history.should include(:before_destroy)
83
+ doc.history.should include(:after_destroy)
84
+ end
85
+ end
86
+ end