jnunemaker-mongomapper 0.1.0

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,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