mongomatic 0.6.3 → 0.6.4

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