mongomatic 0.6.3 → 0.6.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/mongomatic/base.rb +7 -0
- data/lib/mongomatic/m_hash.rb +149 -2
- data/lib/mongomatic/modifiers.rb +3 -2
- data/lib/mongomatic/type_converters.rb +146 -0
- data/lib/mongomatic/typed_fields.rb +58 -0
- data/lib/mongomatic.rb +2 -0
- data/test/helper.rb +16 -3
- data/test/test_typed_fields.rb +53 -0
- metadata +7 -3
data/lib/mongomatic/base.rb
CHANGED
@@ -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]
|
data/lib/mongomatic/m_hash.rb
CHANGED
@@ -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 <
|
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
|
data/lib/mongomatic/modifiers.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
-
|
9
|
-
version: 0.6.
|
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-
|
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
|