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