hashme 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/README.md +30 -4
- data/lib/hashme.rb +1 -0
- data/lib/hashme/casted_array.rb +2 -2
- data/lib/hashme/properties.rb +1 -1
- data/lib/hashme/property.rb +10 -33
- data/lib/hashme/property_casting.rb +194 -0
- data/lib/hashme/version.rb +1 -1
- data/spec/hashme/attributes_spec.rb +11 -12
- data/spec/hashme/base_spec.rb +4 -5
- data/spec/hashme/casted_array_spec.rb +20 -17
- data/spec/hashme/properties_spec.rb +21 -21
- data/spec/hashme/property_casting_spec.rb +612 -0
- data/spec/hashme/property_spec.rb +36 -38
- metadata +18 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1baf318f249321f9486052a01db0881479f461c
|
4
|
+
data.tar.gz: f5b623806198098e2d5e4d6fa42c60cb6137498e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bdfc2fea9f19cccd974a2a8d0b8cfad142bb91af0f35d1ba40a91f0fe0049ae085633d48f1abf9e67f211f6bbdb811bb6961e2ebf491f5858bdfcbc4494933b
|
7
|
+
data.tar.gz: 9e1b4fb53dde4cafd41b9e5917a7a7e12ea22b036cd330ae3eebf79b749a89897afe3f18bb346ad1e524ceb91a27f0af9e1e3a5be1e65d255ce983a6f7ca26d2
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/README.md
CHANGED
@@ -33,16 +33,18 @@ class Cat
|
|
33
33
|
|
34
34
|
property :name, String
|
35
35
|
property :description, String
|
36
|
+
property :dob, Date
|
36
37
|
end
|
37
38
|
|
38
39
|
# Do something with it
|
39
|
-
kitty = Cat.new(:name => "Catso", :description => "Meows a lot")
|
40
|
+
kitty = Cat.new(:name => "Catso", :description => "Meows a lot", :dob => '2012-02-03')
|
40
41
|
kitty.name # Catso
|
41
|
-
kitty.to_hash # {:name => "Catso", :description => "Meows a lot"}
|
42
|
-
kitty.to_json # "{\"name\":\"Catso\",\"description\":\"Meows a lot\"}"
|
42
|
+
kitty.to_hash # {:name => "Catso", :description => "Meows a lot", :dob => "2012-02-03"}
|
43
|
+
kitty.to_json # "{\"name\":\"Catso\",\"description\":\"Meows a lot\",\"dob\":\"2012-02-03\"}"
|
43
44
|
|
44
45
|
kitty2 = Cat.new(kitty.to_hash)
|
45
46
|
kitty2.to_hash == kitty.to_hash # true!
|
47
|
+
kitty2.dob.is_a?(Date) # true!
|
46
48
|
````
|
47
49
|
|
48
50
|
Models can also be nested, which is probably the most useful part:
|
@@ -75,6 +77,24 @@ kennel = Kennel.new(store)
|
|
75
77
|
kennel.cats.length == 2 # true!
|
76
78
|
````
|
77
79
|
|
80
|
+
Active Model Validation is included out the box:
|
81
|
+
|
82
|
+
````ruby
|
83
|
+
class User
|
84
|
+
include Hashme
|
85
|
+
|
86
|
+
property :name, String
|
87
|
+
property :email, String
|
88
|
+
|
89
|
+
validates :name, :email, presence: true
|
90
|
+
end
|
91
|
+
|
92
|
+
u = User.new(name: "Sam")
|
93
|
+
u.valid? # false !
|
94
|
+
u.errors.first # [:email, "can't be blank"]
|
95
|
+
````
|
96
|
+
|
97
|
+
|
78
98
|
## Contributing
|
79
99
|
|
80
100
|
1. Fork it
|
@@ -89,10 +109,16 @@ kennel.cats.length == 2 # true!
|
|
89
109
|
|
90
110
|
## History
|
91
111
|
|
112
|
+
### 0.2.0 - 2016-06-02
|
113
|
+
|
114
|
+
* Added support for advanced type casting, copy stuff from CouchRest Model.
|
115
|
+
* Upgrade to latest rspec version.
|
116
|
+
* Removed `Property#cast` and switch to just `Property#build`.
|
117
|
+
|
92
118
|
### 0.1.2 - 2014-03-10
|
93
119
|
|
94
120
|
* Set default property values on object initialization.
|
95
|
-
* Refactoring to use `
|
121
|
+
* Refactoring to use `class_attribute` for properties hash for improved inheritance.
|
96
122
|
|
97
123
|
### 0.1.1 - 2014-01-21
|
98
124
|
|
data/lib/hashme.rb
CHANGED
data/lib/hashme/casted_array.rb
CHANGED
@@ -20,7 +20,7 @@ module Hashme
|
|
20
20
|
:encode_json, :as_json, :to_json,
|
21
21
|
:inspect, :any?
|
22
22
|
|
23
|
-
def initialize(
|
23
|
+
def initialize(property, owner, values = [])
|
24
24
|
@_array = []
|
25
25
|
self.casted_by = owner
|
26
26
|
self.casted_by_property = property
|
@@ -54,7 +54,7 @@ module Hashme
|
|
54
54
|
protected
|
55
55
|
|
56
56
|
def instantiate_and_build(obj)
|
57
|
-
casted_by_property.build(
|
57
|
+
casted_by_property.build(self, obj)
|
58
58
|
end
|
59
59
|
|
60
60
|
end
|
data/lib/hashme/properties.rb
CHANGED
data/lib/hashme/property.rb
CHANGED
@@ -1,24 +1,22 @@
|
|
1
1
|
module Hashme
|
2
2
|
class Property
|
3
3
|
|
4
|
-
|
4
|
+
attr_reader :name, :type, :default, :array
|
5
5
|
|
6
6
|
def initialize(name, type, opts = {})
|
7
|
-
|
7
|
+
@name = name.to_sym
|
8
8
|
|
9
9
|
# Always set type to base type
|
10
10
|
if type.is_a?(Array) && !type.first.nil?
|
11
|
-
@
|
12
|
-
|
11
|
+
@array = true
|
12
|
+
@type = type.first
|
13
13
|
else
|
14
|
-
@
|
15
|
-
|
14
|
+
@array = false
|
15
|
+
@type = type
|
16
16
|
end
|
17
17
|
|
18
|
-
self.type = klass
|
19
|
-
|
20
18
|
# Handle options
|
21
|
-
|
19
|
+
@default = opts[:default]
|
22
20
|
end
|
23
21
|
|
24
22
|
def to_s
|
@@ -29,34 +27,13 @@ module Hashme
|
|
29
27
|
name
|
30
28
|
end
|
31
29
|
|
32
|
-
# Use cast method when we do not know if we may need to handle a
|
33
|
-
# casted array of objects.
|
34
|
-
def cast(owner, value)
|
35
|
-
if use_casted_array?
|
36
|
-
CastedArray.new(owner, self, value)
|
37
|
-
else
|
38
|
-
build(owner, value)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
30
|
# Build a new object of the type defined by the property.
|
43
|
-
# Will not deal create CastedArrays!
|
44
31
|
def build(owner, value)
|
45
|
-
|
46
|
-
|
47
|
-
obj = value
|
48
|
-
elsif type == Date
|
49
|
-
obj = type.parse(value)
|
32
|
+
if array && value.is_a?(Array)
|
33
|
+
CastedArray.new(self, owner, value)
|
50
34
|
else
|
51
|
-
|
35
|
+
PropertyCasting.cast(self, owner, value)
|
52
36
|
end
|
53
|
-
obj.casted_by = owner if obj.respond_to?(:casted_by=)
|
54
|
-
obj.casted_by_property = self if obj.respond_to?(:casted_by_property=)
|
55
|
-
obj
|
56
|
-
end
|
57
|
-
|
58
|
-
def use_casted_array?
|
59
|
-
@_use_casted_array
|
60
37
|
end
|
61
38
|
|
62
39
|
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module Hashme
|
2
|
+
|
3
|
+
# Special property casting for reveiving data from sources without Ruby types, such as query
|
4
|
+
# parameters from an API or JSON documents.
|
5
|
+
#
|
6
|
+
# Most of this code is stolen from CouchRest Model typecasting, with a few simplifications.
|
7
|
+
module PropertyCasting
|
8
|
+
extend self
|
9
|
+
|
10
|
+
CASTABLE_TYPES = [String, Symbol, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class]
|
11
|
+
|
12
|
+
# Automatically typecast the provided value into an instance of the provided type.
|
13
|
+
def cast(property, owner, value)
|
14
|
+
return nil if value.nil?
|
15
|
+
type = property.type
|
16
|
+
if value.instance_of?(type) || type == Object
|
17
|
+
value
|
18
|
+
elsif CASTABLE_TYPES.include?(type)
|
19
|
+
send('typecast_to_'+type.to_s.downcase, value)
|
20
|
+
else
|
21
|
+
# Complex objects we don't know how to cast
|
22
|
+
type.new(value).tap do |obj|
|
23
|
+
obj.casted_by = owner if obj.respond_to?(:casted_by=)
|
24
|
+
obj.casted_by_property = property if obj.respond_to?(:casted_by_property=)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
# Typecast a value to an Integer
|
32
|
+
def typecast_to_integer(value)
|
33
|
+
typecast_to_numeric(value, :to_i)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Typecast a value to a BigDecimal
|
37
|
+
def typecast_to_bigdecimal(value)
|
38
|
+
typecast_to_numeric(value, :to_d)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Typecast a value to a Float
|
42
|
+
def typecast_to_float(value)
|
43
|
+
typecast_to_numeric(value, :to_f)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Convert some kind of object to a number that of the type
|
47
|
+
# provided.
|
48
|
+
#
|
49
|
+
# When a string is provided, It'll attempt to filter out
|
50
|
+
# region specific details such as commas instead of points
|
51
|
+
# for decimal places, text units, and anything else that is
|
52
|
+
# not a number and a human could make out.
|
53
|
+
#
|
54
|
+
# Esentially, the aim is to provide some kind of sanitary
|
55
|
+
# conversion from values in incoming http forms.
|
56
|
+
#
|
57
|
+
# If what we get makes no sense at all, nil it.
|
58
|
+
def typecast_to_numeric(value, method)
|
59
|
+
if value.is_a?(String)
|
60
|
+
value = value.strip.gsub(/,/, '.').gsub(/[^\d\-\.]/, '').gsub(/\.(?!\d*\Z)/, '')
|
61
|
+
value.empty? ? nil : value.send(method)
|
62
|
+
elsif value.respond_to?(method)
|
63
|
+
value.send(method)
|
64
|
+
else
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Typecast a value to a String
|
70
|
+
def typecast_to_string(value)
|
71
|
+
value.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def typecast_to_symbol(value)
|
75
|
+
value.is_a?(Symbol) || !value.to_s.empty? ? value.to_sym : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# Typecast a value to a true or false
|
79
|
+
def typecast_to_trueclass(value)
|
80
|
+
if value.kind_of?(Integer)
|
81
|
+
return true if value == 1
|
82
|
+
return false if value == 0
|
83
|
+
elsif value.respond_to?(:to_s)
|
84
|
+
return true if %w[ true 1 t ].include?(value.to_s.downcase)
|
85
|
+
return false if %w[ false 0 f ].include?(value.to_s.downcase)
|
86
|
+
end
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
# Typecasts an arbitrary value to a DateTime.
|
91
|
+
# Handles both Hashes and DateTime instances.
|
92
|
+
# This is slow!! Use Time instead.
|
93
|
+
def typecast_to_datetime(value)
|
94
|
+
if value.is_a?(Hash)
|
95
|
+
typecast_hash_to_datetime(value)
|
96
|
+
else
|
97
|
+
DateTime.parse(value.to_s)
|
98
|
+
end
|
99
|
+
rescue ArgumentError
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Typecasts an arbitrary value to a Date
|
104
|
+
# Handles both Hashes and Date instances.
|
105
|
+
def typecast_to_date(value)
|
106
|
+
if value.is_a?(Hash)
|
107
|
+
typecast_hash_to_date(value)
|
108
|
+
elsif value.is_a?(Time) # sometimes people think date is time!
|
109
|
+
value.to_date
|
110
|
+
elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
|
111
|
+
# Faster than parsing the date
|
112
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
113
|
+
else
|
114
|
+
Date.parse(value)
|
115
|
+
end
|
116
|
+
rescue ArgumentError
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
# Typecasts an arbitrary value to a Time
|
121
|
+
# Handles both Hashes and Time instances.
|
122
|
+
def typecast_to_time(value)
|
123
|
+
case value
|
124
|
+
when Float # JSON oj already parses Time, FTW.
|
125
|
+
Time.at(value).utc
|
126
|
+
when Hash
|
127
|
+
typecast_hash_to_time(value)
|
128
|
+
else
|
129
|
+
typecast_iso8601_string_to_time(value.to_s)
|
130
|
+
end
|
131
|
+
rescue ArgumentError
|
132
|
+
nil
|
133
|
+
rescue TypeError
|
134
|
+
nil
|
135
|
+
end
|
136
|
+
|
137
|
+
def typecast_iso8601_string_to_time(string)
|
138
|
+
if (string =~ /(\d{4})[\-\/](\d{2})[\-\/](\d{2})[T\s](\d{2}):(\d{2}):(\d{2}(\.\d+)?)(Z| ?([\+\s\-])?(\d{2}):?(\d{2}))?/)
|
139
|
+
# $1 = year
|
140
|
+
# $2 = month
|
141
|
+
# $3 = day
|
142
|
+
# $4 = hours
|
143
|
+
# $5 = minutes
|
144
|
+
# $6 = seconds (with $7 for fraction)
|
145
|
+
# $8 = UTC or Timezone
|
146
|
+
# $9 = time zone direction
|
147
|
+
# $10 = tz difference hours
|
148
|
+
# $11 = tz difference minutes
|
149
|
+
|
150
|
+
if $8 == 'Z' || $8.to_s.empty?
|
151
|
+
Time.utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_r)
|
152
|
+
else
|
153
|
+
Time.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_r, "#{$9 == '-' ? '-' : '+'}#{$10}:#{$11}")
|
154
|
+
end
|
155
|
+
else
|
156
|
+
Time.parse(string)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Creates a DateTime instance from a Hash with keys :year, :month, :day,
|
161
|
+
# :hour, :min, :sec
|
162
|
+
def typecast_hash_to_datetime(value)
|
163
|
+
DateTime.new(*extract_time(value))
|
164
|
+
end
|
165
|
+
|
166
|
+
# Creates a Date instance from a Hash with keys :year, :month, :day
|
167
|
+
def typecast_hash_to_date(value)
|
168
|
+
Date.new(*extract_time(value)[0, 3].map(&:to_i))
|
169
|
+
end
|
170
|
+
|
171
|
+
# Creates a Time instance from a Hash with keys :year, :month, :day,
|
172
|
+
# :hour, :min, :sec
|
173
|
+
def typecast_hash_to_time(value)
|
174
|
+
Time.utc(*extract_time(value))
|
175
|
+
end
|
176
|
+
|
177
|
+
# Extracts the given args from the hash. If a value does not exist, it
|
178
|
+
# uses the value of Time.now.
|
179
|
+
def extract_time(value)
|
180
|
+
now = Time.now
|
181
|
+
[:year, :month, :day, :hour, :min, :sec].map do |segment|
|
182
|
+
typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Typecast a value to a Class
|
187
|
+
def typecast_to_class(value)
|
188
|
+
value.to_s.constantize
|
189
|
+
rescue NameError
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
data/lib/hashme/version.rb
CHANGED
@@ -22,21 +22,20 @@ describe Hashme::Attributes do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
it "should assign some of the basic hash methods" do
|
25
|
-
(@obj == @hash).
|
26
|
-
@obj.eql
|
27
|
-
@obj.
|
28
|
-
@obj.
|
29
|
-
@obj.to_hash.should eql(@hash)
|
25
|
+
expect(@obj == @hash).to be_truthy
|
26
|
+
expect(@obj.keys).to eql(@hash.keys)
|
27
|
+
expect(@obj.values).to eql(@hash.values)
|
28
|
+
expect(@obj.to_hash).to eql(@hash)
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
33
32
|
describe "#[]=" do
|
34
33
|
it "should assign values to attributes hash" do
|
35
34
|
@obj[:akey] = "test"
|
36
|
-
attribs[:akey].
|
35
|
+
expect(attribs[:akey]).to eql("test")
|
37
36
|
@obj['akey'] = "anothertest"
|
38
|
-
attribs[:akey].
|
39
|
-
attribs['akey'].
|
37
|
+
expect(attribs[:akey]).to eql("anothertest")
|
38
|
+
expect(attribs['akey']).to be_nil
|
40
39
|
end
|
41
40
|
end
|
42
41
|
|
@@ -44,7 +43,7 @@ describe Hashme::Attributes do
|
|
44
43
|
it "should remove attribtue entry" do
|
45
44
|
@obj[:key] = 'value'
|
46
45
|
@obj.delete(:key)
|
47
|
-
@obj[:key].
|
46
|
+
expect(@obj[:key]).to be_nil
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
@@ -52,7 +51,7 @@ describe Hashme::Attributes do
|
|
52
51
|
it "should duplicate attributes" do
|
53
52
|
@obj[:key] = 'value'
|
54
53
|
@obj2 = @obj.dup
|
55
|
-
@obj2.send(:_attributes).object_id.
|
54
|
+
expect(@obj2.send(:_attributes).object_id).to_not eql(@obj.send(:_attributes).object_id)
|
56
55
|
end
|
57
56
|
end
|
58
57
|
|
@@ -60,7 +59,7 @@ describe Hashme::Attributes do
|
|
60
59
|
it "should clone attributes" do
|
61
60
|
@obj[:key] = 'value'
|
62
61
|
@obj2 = @obj.clone
|
63
|
-
@obj2.send(:_attributes).object_id.
|
62
|
+
expect(@obj2.send(:_attributes).object_id).to_not eql(@obj.send(:_attributes).object_id)
|
64
63
|
end
|
65
64
|
end
|
66
65
|
|
@@ -70,7 +69,7 @@ describe Hashme::Attributes do
|
|
70
69
|
it "should provide something useful" do
|
71
70
|
@obj[:key1] = 'value1'
|
72
71
|
@obj[:key2] = 'value2'
|
73
|
-
@obj.inspect.
|
72
|
+
expect(@obj.inspect).to match(/#<.+ key1: "value1", key2: "value2">/)
|
74
73
|
end
|
75
74
|
|
76
75
|
end
|