mongomatic 0.6.3 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,7 @@ module Mongomatic
3
3
  include Mongomatic::Modifiers
4
4
  include Mongomatic::Util
5
5
  include Mongomatic::ActiveModelCompliancy
6
+ include Mongomatic::TypedFields
6
7
 
7
8
  class << self
8
9
  # Returns this models own db attribute if set, otherwise will return Mongomatic.db
@@ -108,6 +109,7 @@ module Mongomatic
108
109
  end
109
110
 
110
111
  def valid?
112
+ check_typed_fields!
111
113
  self.errors = Mongomatic::Errors.new
112
114
  do_callback(:before_validate)
113
115
  validate
@@ -136,6 +138,11 @@ module Mongomatic
136
138
  hash.has_key?(field)
137
139
  end
138
140
 
141
+ def set_value_for_key(key, value)
142
+ field, hash = hash_for_field(key.to_s)
143
+ hash[field] = value
144
+ end
145
+
139
146
  def value_for_key(key)
140
147
  field, hash = hash_for_field(key.to_s, true)
141
148
  hash[field]
@@ -1,4 +1,151 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ # This class has dubious semantics and we only have it so that
4
+ # people can write params[:key] instead of params['key']
5
+ # and they get the same value for both keys.
6
+
1
7
  module Mongomatic
2
- class MHash < HashWithIndifferentAccess
8
+ class MHash < Hash
9
+ def extractable_options?
10
+ true
11
+ end
12
+
13
+ def initialize(constructor = {})
14
+ if constructor.is_a?(Hash)
15
+ super()
16
+ update(constructor)
17
+ else
18
+ super(constructor)
19
+ end
20
+ end
21
+
22
+ def default(key = nil)
23
+ if key.is_a?(Symbol) && include?(key = key.to_s)
24
+ self[key]
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def self.new_from_hash_copying_default(hash)
31
+ Mongomatic::MHash.new(hash).tap do |new_hash|
32
+ new_hash.default = hash.default
33
+ end
34
+ end
35
+
36
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
37
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
38
+
39
+ # Assigns a new value to the hash:
40
+ #
41
+ # hash = MHash.new
42
+ # hash[:key] = "value"
43
+ #
44
+ def []=(key, value)
45
+ regular_writer(convert_key(key), convert_value(value))
46
+ end
47
+
48
+ # Updates the instantized hash with values from the second:
49
+ #
50
+ # hash_1 = MHash.new
51
+ # hash_1[:key] = "value"
52
+ #
53
+ # hash_2 = MHash.new
54
+ # hash_2[:key] = "New Value!"
55
+ #
56
+ # hash_1.update(hash_2) # => {"key"=>"New Value!"}
57
+ #
58
+ def update(other_hash)
59
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
60
+ self
61
+ end
62
+
63
+ alias_method :merge!, :update
64
+
65
+ # Checks the hash for a key matching the argument passed in:
66
+ #
67
+ # hash = MHash.new
68
+ # hash["key"] = "value"
69
+ # hash.key? :key # => true
70
+ # hash.key? "key" # => true
71
+ #
72
+ def key?(key)
73
+ super(convert_key(key))
74
+ end
75
+
76
+ alias_method :include?, :key?
77
+ alias_method :has_key?, :key?
78
+ alias_method :member?, :key?
79
+
80
+ # Fetches the value for the specified key, same as doing hash[key]
81
+ def fetch(key, *extras)
82
+ super(convert_key(key), *extras)
83
+ end
84
+
85
+ # Returns an array of the values at the specified indices:
86
+ #
87
+ # hash = MHash.new
88
+ # hash[:a] = "x"
89
+ # hash[:b] = "y"
90
+ # hash.values_at("a", "b") # => ["x", "y"]
91
+ #
92
+ def values_at(*indices)
93
+ indices.collect {|key| self[convert_key(key)]}
94
+ end
95
+
96
+ # Returns an exact copy of the hash.
97
+ def dup
98
+ MHash.new(self)
99
+ end
100
+
101
+ # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
102
+ # Does not overwrite the existing hash.
103
+ def merge(hash)
104
+ self.dup.update(hash)
105
+ end
106
+
107
+ # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
108
+ # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
109
+ def reverse_merge(other_hash)
110
+ super self.class.new_from_hash_copying_default(other_hash)
111
+ end
112
+
113
+ def reverse_merge!(other_hash)
114
+ replace(reverse_merge( other_hash ))
115
+ end
116
+
117
+ # Removes a specified key from the hash.
118
+ def delete(key)
119
+ super(convert_key(key))
120
+ end
121
+
122
+ def stringify_keys!; self end
123
+ def stringify_keys; dup end
124
+ undef :symbolize_keys!
125
+ def symbolize_keys; to_hash.symbolize_keys end
126
+ def to_options!; self end
127
+
128
+ # Convert to a Hash with String keys.
129
+ def to_hash
130
+ Hash.new(default).merge!(self)
131
+ end
132
+
133
+ protected
134
+ def convert_key(key)
135
+ key.kind_of?(Symbol) ? key.to_s : key
136
+ end
137
+
138
+ def convert_value(value)
139
+ case value
140
+ when Hash
141
+ self.class.new_from_hash_copying_default(value)
142
+ when Array
143
+ value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e }
144
+ else
145
+ value
146
+ end
147
+ end
3
148
  end
4
- end
149
+ end
150
+
151
+ MHash = Mongomatic::MHash
@@ -150,7 +150,7 @@ module Mongomatic
150
150
  # user.set("name", "Ben")
151
151
  def set(field, val, update_opts={}, safe=false)
152
152
  mongo_field = field.to_s
153
- field, hash = hash_for_field(field.to_s)
153
+ #field, hash = hash_for_field(field.to_s)
154
154
 
155
155
  op = { "$set" => { mongo_field => val } }
156
156
  res = true
@@ -158,7 +158,8 @@ module Mongomatic
158
158
  safe == true ? res = update!(update_opts, op) : update(update_opts, op)
159
159
 
160
160
  if res
161
- hash[field] = val
161
+ set_value_for_key(field.to_s, val)
162
+ #hash[field] = val
162
163
  true
163
164
  end
164
165
  end
@@ -0,0 +1,146 @@
1
+ module Mongomatic
2
+ module TypeConverters
3
+ class CannotCastValue < RuntimeError; end
4
+
5
+ def self.for_type(type)
6
+ eval "Mongomatic::TypeConverters::#{type.to_s.camelize}"
7
+ end
8
+
9
+ class Base
10
+ def initialize(orig_val)
11
+ @orig_val = orig_val
12
+ end
13
+
14
+ def type_match?
15
+ raise "abstract"
16
+ end
17
+
18
+ def cast
19
+ if type_match?
20
+ @orig_val
21
+ else
22
+ convert_orig_val || raise(CannotCastValue)
23
+ end
24
+ end
25
+
26
+ def convert_orig_val
27
+ raise "abstract"
28
+ end
29
+ end
30
+
31
+ class String < Base
32
+ def type_match?
33
+ @orig_val.class.to_s == "String"
34
+ end
35
+
36
+ def convert_orig_val
37
+ @orig_val.respond_to?(:to_s) ? @orig_val.to_s : nil
38
+ end
39
+ end
40
+
41
+ class Float < Base
42
+ def type_match?
43
+ @orig_val.class.to_s == "Float"
44
+ end
45
+
46
+ def convert_orig_val
47
+ @orig_val.respond_to?(:to_f) ? @orig_val.to_f : nil
48
+ end
49
+ end
50
+
51
+ class Fixnum < Base
52
+ def type_match?
53
+ @orig_val.class.to_s == "Fixnum"
54
+ end
55
+
56
+ def convert_orig_val
57
+ @orig_val.respond_to?(:to_i) ? @orig_val.to_i : nil
58
+ end
59
+ end
60
+
61
+ class Array < Base
62
+ def type_match?
63
+ @orig_val.class.to_s == "Array"
64
+ end
65
+
66
+ def convert_orig_val
67
+ @orig_val.respond_to?(:to_a) ? @orig_val.to_a : nil
68
+ end
69
+ end
70
+
71
+ class Hash < Base
72
+ def type_match?
73
+ @orig_val.class.to_s == "Hash"
74
+ end
75
+
76
+ def convert_orig_val
77
+ [:to_h, :to_hash].each do |meth|
78
+ res = (@orig_val.respond_to?(meth) ? @orig_val.send(meth) : nil)
79
+ return res if !res.nil?
80
+ end; nil
81
+ end
82
+ end
83
+
84
+ class Bool < Base
85
+ def type_match?
86
+ @orig_val == true || @orig_val == false
87
+ end
88
+
89
+ def convert_orig_val
90
+ s_val = @orig_val.to_s.downcase
91
+ if %w(1 t true y yes).include?(s_val)
92
+ true
93
+ elsif %w(0 f false n no).include?(s_val)
94
+ false
95
+ else
96
+ nil
97
+ end
98
+ end
99
+ end
100
+
101
+ class Time < Base
102
+ def type_match?
103
+ @orig_val.class.to_s == "Time"
104
+ end
105
+
106
+ def convert_orig_val
107
+ Time.parse(@orig_val.to_s)
108
+ rescue ArgumentError => e
109
+ nil
110
+ end
111
+ end
112
+
113
+ class Regex < Base
114
+ def type_match?
115
+ @orig_val.class.to_s == "Regexp"
116
+ end
117
+
118
+ def convert_orig_val
119
+ Regexp.new(@orig_val.to_s)
120
+ end
121
+ end
122
+
123
+ class Symbol < Base
124
+ def type_match?
125
+ @orig_val.class.to_s == "Symbol"
126
+ end
127
+
128
+ def convert_orig_val
129
+ @orig_val.respond_to?(:to_sym) ? @orig_val.to_sym : nil
130
+ end
131
+ end
132
+
133
+ class ObjectId < Base
134
+ def type_match?
135
+ @orig_val.class.to_s == "BSON::ObjectId"
136
+ end
137
+
138
+ def convert_orig_val
139
+ BSON::ObjectId(@orig_val.to_s)
140
+ rescue BSON::InvalidObjectId => e
141
+ nil
142
+ end
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,58 @@
1
+ module Mongomatic
2
+ # = Typed Fields
3
+ # Explicitly specify the field types in your document. This is completely optional.
4
+ # You can also set whether or not we should try to automatically cast a type to the
5
+ # desired type.
6
+ # = Examples
7
+ # typed_field "age", :type => :fixnum, :cast => true
8
+ # typed_field "manufacturer.name", :type => :string, :cast => false
9
+ module TypedFields
10
+ class InvalidType < RuntimeError; end
11
+
12
+ KNOWN_TYPES = [:string, :float, :fixnum, :array, :hash, :bool,
13
+ :time, :regex, :symbol, :object_id]
14
+
15
+ def self.included(base)
16
+ base.send(:extend, ClassMethods)
17
+ base.send(:include, InstanceMethods)
18
+ end
19
+
20
+ module ClassMethods
21
+ def typed_field(name, opts)
22
+ unless Mongomatic::TypedFields::KNOWN_TYPES.include?(opts[:type])
23
+ raise Mongomatic::TypedFields::Invalidtype, "#{opts[:type]}"
24
+ end
25
+
26
+ opts = {:cast => true}.merge(opts)
27
+
28
+ @typed_fields ||= {}
29
+ @typed_fields[name] = opts
30
+ end
31
+
32
+ def typed_fields
33
+ @typed_fields || {}
34
+ end
35
+ end # ClassMethods
36
+
37
+ module InstanceMethods
38
+
39
+ def check_typed_fields!
40
+ self.class.typed_fields.each do |name, opts|
41
+ cast_or_raise_typed_field(name, opts)
42
+ end
43
+ end
44
+
45
+ def cast_or_raise_typed_field(name, opts)
46
+ val = value_for_key(name.to_s); return if val.nil?
47
+ type = opts[:type].to_sym
48
+ try_cast = opts[:cast]
49
+
50
+ converter = Mongomatic::TypeConverters.for_type(type).new(val)
51
+ return true if converter.type_match?
52
+ raise(InvalidType, "#{name} should be a :#{type}") unless try_cast
53
+ set_value_for_key(name, converter.cast)
54
+ end
55
+
56
+ end # InstanceMethods
57
+ end # TypedFields
58
+ end # Mongomatic
data/lib/mongomatic.rb CHANGED
@@ -37,4 +37,6 @@ require "#{File.dirname(__FILE__)}/mongomatic/modifiers"
37
37
  require "#{File.dirname(__FILE__)}/mongomatic/errors"
38
38
  require "#{File.dirname(__FILE__)}/mongomatic/expectations"
39
39
  require "#{File.dirname(__FILE__)}/mongomatic/active_model_compliancy"
40
+ require "#{File.dirname(__FILE__)}/mongomatic/type_converters"
41
+ require "#{File.dirname(__FILE__)}/mongomatic/typed_fields"
40
42
  require "#{File.dirname(__FILE__)}/mongomatic/base"
data/test/helper.rb CHANGED
@@ -2,9 +2,10 @@ require 'rubygems'
2
2
  gem 'minitest', "~> 2.0"
3
3
  require 'pp'
4
4
 
5
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
- $LOAD_PATH.unshift(File.dirname(__FILE__))
7
- require 'mongomatic'
5
+ # $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ # $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require "#{File.dirname(__FILE__)}/../lib/mongomatic"
8
+ #require 'mongomatic'
8
9
 
9
10
  Mongomatic.db = Mongo::Connection.new.db("mongomatic_test")
10
11
 
@@ -103,3 +104,15 @@ class Foobar < Mongomatic::Base
103
104
  errors << "missing style" if self["style"].blank?
104
105
  end
105
106
  end
107
+
108
+ class Rig < Mongomatic::Base
109
+ # :cast => true, :raise => false is the default
110
+ typed_field "age", :type => :fixnum, :cast => true
111
+ typed_field "manufacturer.name", :type => :string, :cast => true
112
+ typed_field "manufacturer.phone", :type => :string, :cast => false
113
+ typed_field "waist_measurement", :type => :float, :cast => true
114
+ typed_field "friends_rig_id", :type => :object_id, :cast => true
115
+ end
116
+
117
+
118
+
@@ -0,0 +1,53 @@
1
+ require 'helper'
2
+ require 'minitest/autorun'
3
+
4
+ class TestTypedFields < MiniTest::Unit::TestCase
5
+ def setup
6
+ Rig.collection.drop
7
+ end
8
+
9
+ def test_raising_error_on_invalid_type
10
+ r = Rig.new
11
+ assert r["manufacturer"].blank?
12
+ r["manufacturer"] = { "phone" => 123 }
13
+ assert_raises(Mongomatic::TypedFields::InvalidType) { r.valid? }
14
+ r["manufacturer"] = {}
15
+ r["manufacturer"]["phone"] = "(800) 123 456 789"
16
+ assert_equal true, r.valid?
17
+ assert_equal "(800) 123 456 789", r["manufacturer"]["phone"]
18
+ end
19
+
20
+ def test_cast_string
21
+ r = Rig.new
22
+ r["manufacturer"] = {}
23
+ r["manufacturer"]["name"] = ["Wings","Parachuting","Company"]
24
+ assert_equal ["Wings","Parachuting","Company"], r["manufacturer"]["name"]
25
+ assert r.valid?
26
+ assert_equal ["Wings","Parachuting","Company"].to_s, r["manufacturer"]["name"]
27
+ end
28
+
29
+ def test_cast_number
30
+ r = Rig.new
31
+ r["age"] = "4"
32
+ assert_equal "4", r["age"]
33
+ assert r.valid?
34
+ assert_equal 4, r["age"]
35
+ end
36
+
37
+ def test_cast_float
38
+ r = Rig.new
39
+ r["waist_measurement"] = "34.3"
40
+ assert_equal "34.3", r["waist_measurement"]
41
+ assert r.valid?
42
+ assert_equal 34.3, r["waist_measurement"]
43
+ end
44
+
45
+ def test_cast_object_id
46
+ r = Rig.new
47
+ assert r.insert
48
+ r2 = Rig.new
49
+ r2["friends_rig_id"] = r["_id"].to_s
50
+ r2.insert
51
+ assert_equal "BSON::ObjectId", r2["friends_rig_id"].class.to_s
52
+ end
53
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 6
8
- - 3
9
- version: 0.6.3
8
+ - 4
9
+ version: 0.6.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Ben Myles
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-11-12 00:00:00 -06:00
17
+ date: 2010-11-16 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -99,6 +99,8 @@ files:
99
99
  - lib/mongomatic/expectations/present.rb
100
100
  - lib/mongomatic/m_hash.rb
101
101
  - lib/mongomatic/modifiers.rb
102
+ - lib/mongomatic/type_converters.rb
103
+ - lib/mongomatic/typed_fields.rb
102
104
  - lib/mongomatic/util.rb
103
105
  - LICENSE
104
106
  - README.rdoc
@@ -108,6 +110,7 @@ files:
108
110
  - test/test_misc.rb
109
111
  - test/test_modifiers.rb
110
112
  - test/test_persistence.rb
113
+ - test/test_typed_fields.rb
111
114
  - test/test_validations.rb
112
115
  has_rdoc: true
113
116
  homepage: http://mongomatic.com/
@@ -148,4 +151,5 @@ test_files:
148
151
  - test/test_misc.rb
149
152
  - test/test_modifiers.rb
150
153
  - test/test_persistence.rb
154
+ - test/test_typed_fields.rb
151
155
  - test/test_validations.rb